From 4d2a5aeb93beb362926d1fb9ada86d0fd9098b82 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 12:55:26 -0400 Subject: [PATCH 01/25] Initial koin setup --- app/build.gradle.kts | 11 + .../damontecres/stashapp/StashApplication.kt | 10 + .../damontecres/stashapp/di/AppModule.kt | 96 ++++ .../stashapp/di/server/MutationEngine.kt | 515 +++++++++++++++++ .../stashapp/di/server/QueryEngine.kt | 522 ++++++++++++++++++ .../stashapp/di/server/ServerRepository.kt | 5 + .../stashapp/di/server/StashApi.kt | 179 ++++++ .../stashapp/di/server/StashEngine.kt | 64 +++ .../stashapp/di/server/StashServer.kt | 48 ++ .../stashapp/di/server/SubscriptionEngine.kt | 66 +++ .../damontecres/stashapp/util/Constants.kt | 2 +- gradle/libs.versions.toml | 13 + 12 files changed, 1530 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/ServerRepository.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/StashApi.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/StashServer.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6a0e8f5d..9f485a77 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,6 +31,7 @@ plugins { alias(libs.plugins.room) alias(libs.plugins.compose.compiler) alias(libs.plugins.protobuf) + alias(libs.plugins.koin.compiler) } val gitTags = @@ -319,6 +320,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/java/com/github/damontecres/stashapp/StashApplication.kt b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt index 5c0810a4..631a8ef6 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/StashApplication.kt @@ -29,8 +29,13 @@ 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 timber.log.Timber +@KoinApplication class StashApplication : Application() { @OptIn(ExperimentalComposeRuntimeApi::class) override fun onCreate() { @@ -109,6 +114,11 @@ class StashApplication : Application() { ProcessLifecycleOwner.get().lifecycle.addObserver(LifecycleObserverImpl()) + startKoin { + androidLogger() + androidContext(this@StashApplication) + } + val prefs = PreferenceManager.getDefaultSharedPreferences(this) val currentVersion = prefs.getString(VERSION_NAME_CURRENT_KEY, null) val currentVersionCode = prefs.getLong(VERSION_CODE_CURRENT_KEY, -1) 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 00000000..37a58875 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt @@ -0,0 +1,96 @@ +package com.github.damontecres.stashapp.di + +import android.content.Context +import android.os.Build +import co.touchlab.kermit.Logger +import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.di.server.ServerRepository +import com.github.damontecres.stashapp.util.Constants +import com.github.damontecres.stashapp.util.joinNotNullOrBlank +import com.github.damontecres.stashapp.util.joinValueNotNull +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 + val request = + if (server.apiKey != null) { + val isStashUrl = + it + .request() + .url + .toString() + .startsWith(server.serverRoot) + if (isStashUrl) { + // Only set the API Key if the target URL is the stash server + it + .request() + .newBuilder() + .addHeader(Constants.STASH_API_HEADER, server.cleanedApiKey!!) + .build() + } else { + it.request() + } + } else { + it.request() + } + it.proceed(request) + }.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)" +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt new file mode 100644 index 00000000..73f01b47 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt @@ -0,0 +1,515 @@ +package com.github.damontecres.stashappapp.di.server + +import co.touchlab.kermit.Logger +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Mutation +import com.apollographql.apollo.api.Optional +import com.github.damontecres.stashapp.api.CreateGroupMutation +import com.github.damontecres.stashapp.api.CreateMarkerMutation +import com.github.damontecres.stashapp.api.CreatePerformerMutation +import com.github.damontecres.stashapp.api.CreateStudioMutation +import com.github.damontecres.stashapp.api.CreateTagMutation +import com.github.damontecres.stashapp.api.DeleteMarkerMutation +import com.github.damontecres.stashapp.api.DeleteSceneMutation +import com.github.damontecres.stashapp.api.ImageDecrementOMutation +import com.github.damontecres.stashapp.api.ImageIncrementOMutation +import com.github.damontecres.stashapp.api.ImageResetOMutation +import com.github.damontecres.stashapp.api.InstallPackagesMutation +import com.github.damontecres.stashapp.api.SaveFilterMutation +import com.github.damontecres.stashapp.api.SceneAddOMutation +import com.github.damontecres.stashapp.api.SceneAddPlayCountMutation +import com.github.damontecres.stashapp.api.SceneDeleteOMutation +import com.github.damontecres.stashapp.api.SceneResetOMutation +import com.github.damontecres.stashapp.api.SceneSaveActivityMutation +import com.github.damontecres.stashapp.api.SceneUpdateMutation +import com.github.damontecres.stashapp.api.UpdateGalleryMutation +import com.github.damontecres.stashapp.api.UpdateGroupMutation +import com.github.damontecres.stashapp.api.UpdateImageMutation +import com.github.damontecres.stashapp.api.UpdateMarkerMutation +import com.github.damontecres.stashapp.api.UpdatePerformerMutation +import com.github.damontecres.stashapp.api.UpdateStudioMutation +import com.github.damontecres.stashapp.api.UpdateTagMutation +import com.github.damontecres.stashapp.api.fragment.FullMarkerData +import com.github.damontecres.stashapp.api.fragment.GalleryData +import com.github.damontecres.stashapp.api.fragment.GroupData +import com.github.damontecres.stashapp.api.fragment.PerformerData +import com.github.damontecres.stashapp.api.fragment.SavedFilter +import com.github.damontecres.stashapp.api.fragment.StudioData +import com.github.damontecres.stashapp.api.fragment.TagData +import com.github.damontecres.stashapp.api.type.GalleryUpdateInput +import com.github.damontecres.stashapp.api.type.GroupCreateInput +import com.github.damontecres.stashapp.api.type.GroupUpdateInput +import com.github.damontecres.stashapp.api.type.ImageUpdateInput +import com.github.damontecres.stashapp.api.type.PackageSpecInput +import com.github.damontecres.stashapp.api.type.PackageType +import com.github.damontecres.stashapp.api.type.PerformerCreateInput +import com.github.damontecres.stashapp.api.type.PerformerUpdateInput +import com.github.damontecres.stashapp.api.type.SaveFilterInput +import com.github.damontecres.stashapp.api.type.SceneDestroyInput +import com.github.damontecres.stashapp.api.type.SceneGroupInput +import com.github.damontecres.stashapp.api.type.SceneMarkerCreateInput +import com.github.damontecres.stashapp.api.type.SceneMarkerUpdateInput +import com.github.damontecres.stashapp.api.type.SceneUpdateInput +import com.github.damontecres.stashapp.api.type.StudioCreateInput +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.di.server.StashApi +import com.github.damontecres.stashapp.di.server.StashEngine +import com.github.damontecres.stashapp.util.toSeconds +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.koin.core.annotation.Singleton + +/** + * Class for sending graphql mutations + */ +@Singleton +class MutationEngine( + api: StashApi, +) : StashEngine(api) { + var readOnlyMode = false + + suspend fun executeMutation( + mutation: Mutation, + overrideReadOnly: Boolean = false, + ): ApolloResponse = + withContext(Dispatchers.IO) { + if (!overrideReadOnly && readOnlyMode) { + throw IllegalStateException("Read only mode enabled!") + } + val mutationName = mutation.name() + val id = getRequestId() + + Logger.v { "executeMutation $id $mutationName start" } + val response = client.mutation(mutation).execute() + if (response.data != null) { + Logger.v { "executeMutation $id $mutationName successful" } + return@withContext response + } else if (response.exception != null) { + throw createException(id, mutationName, response.exception!!) { msg, ex -> + MutationException(id, mutationName, msg, ex) + } + } else { + val errorMsgs = response.errors!!.joinToString("\n") { it.message } + Logger.e { "Errors in $id $mutationName: ${response.errors}" } + throw MutationException(id, mutationName, "Error in $mutationName: $errorMsgs") + } + } + + /** + * Saves the resume time for a given scene + * + * @param sceneId the scene ID + * @param position the video playback position in milliseconds + * @param duration how long has the playback been in milliseconds + */ + suspend fun saveSceneActivity( + sceneId: String, + position: Long, + duration: Long? = null, + ): Boolean { + val resumeTime = position / 1000.0 + val playDuration = if (duration != null && duration >= 1) duration / 1000.0 else null + Logger.v { + "SceneSaveActivity sceneId=$sceneId, position=$position, playDuration=$playDuration" + } + val mutation = + SceneSaveActivityMutation( + scene_id = sceneId, + resume_time = resumeTime, + play_duration = playDuration, + ) + val result = executeMutation(mutation, true) + return result.data!!.sceneSaveActivity + } + + suspend fun incrementPlayCount(sceneId: String): Int { + Logger.v { "incrementPlayCount on $sceneId" } + val mutation = SceneAddPlayCountMutation(sceneId, emptyList()) + val result = executeMutation(mutation, true) + return result.data!!.sceneAddPlay.count + } + + suspend fun setTagsOnScene( + sceneId: String, + tagIds: List, + ): SceneUpdateMutation.SceneUpdate? { + Logger.v { "setTagsOnScene sceneId=$sceneId, tagIds=$tagIds" } + val mutation = + SceneUpdateMutation( + input = + SceneUpdateInput( + id = sceneId, + tag_ids = Optional.present(tagIds), + ), + ) + val result = executeMutation(mutation) + return result.data?.sceneUpdate + } + + suspend fun setGroupsOnScene( + sceneId: String, + groupIds: List, + ): SceneUpdateMutation.SceneUpdate? { + Logger.v { "setGroupsOnScene sceneId=$sceneId, tagIds=$groupIds" } + val mutation = + SceneUpdateMutation( + input = + SceneUpdateInput( + id = sceneId, + groups = + Optional.present( + groupIds.map { + SceneGroupInput( + group_id = it, + scene_index = Optional.absent(), + ) + }, + ), + ), + ) + val result = executeMutation(mutation) + return result.data?.sceneUpdate + } + + suspend fun updateMarker(input: SceneMarkerUpdateInput): FullMarkerData? { + val mutation = UpdateMarkerMutation(input = input) + val result = executeMutation(mutation) + return result.data?.sceneMarkerUpdate?.fullMarkerData + } + + suspend fun setTagsOnMarker( + markerId: String, + primaryTagId: String, + tagIds: List, + ): FullMarkerData? { + Logger.v { "setTagsOnMarker markerId=$markerId, primaryTagId=$primaryTagId, tagIds=$tagIds" } + return updateMarker( + SceneMarkerUpdateInput( + id = markerId, + primary_tag_id = Optional.present(primaryTagId), + tag_ids = Optional.present(tagIds), + ), + ) + } + + suspend fun setPerformersOnScene( + sceneId: String, + performerIds: List, + ): SceneUpdateMutation.SceneUpdate? { + Logger.v { "setPerformersOnScene sceneId=$sceneId, performerIds=$performerIds" } + val mutation = + SceneUpdateMutation( + input = + SceneUpdateInput( + id = sceneId, + performer_ids = Optional.present(performerIds), + ), + ) + val result = executeMutation(mutation) + return result.data?.sceneUpdate + } + + suspend fun setStudioOnScene( + sceneId: String, + studioId: String?, + ): SceneUpdateMutation.SceneUpdate? { + Logger.v { "setStudioOnScene sceneId=$sceneId, studioId=$studioId" } + val mutation = + SceneUpdateMutation( + input = + SceneUpdateInput( + id = sceneId, + studio_id = Optional.present(studioId), + ), + ) + val result = executeMutation(mutation) + return result.data?.sceneUpdate + } + + suspend fun setGalleriesOnScene( + sceneId: String, + galleryIds: List, + ): SceneUpdateMutation.SceneUpdate? { + Logger.v { "setGalleriesOnScene sceneId=$sceneId, galleryIds=$galleryIds" } + val mutation = + SceneUpdateMutation( + input = + SceneUpdateInput( + id = sceneId, + gallery_ids = Optional.present(galleryIds), + ), + ) + val result = executeMutation(mutation) + return result.data?.sceneUpdate + } + + suspend fun incrementOCounter(sceneId: String): OCounter { + val mutation = SceneAddOMutation(sceneId, emptyList()) + val result = executeMutation(mutation) + return OCounter(sceneId, result.data!!.sceneAddO.count) + } + + suspend fun decrementOCounter(sceneId: String): OCounter { + val mutation = SceneDeleteOMutation(sceneId, emptyList()) + val result = executeMutation(mutation) + return OCounter(sceneId, result.data!!.sceneDeleteO.count) + } + + suspend fun resetOCounter(sceneId: String): OCounter { + val mutation = SceneResetOMutation(sceneId) + val result = executeMutation(mutation) + return OCounter(sceneId, result.data!!.sceneResetO) + } + + suspend fun createMarker( + sceneId: String, + position: Long, + primaryTagId: String, + ): FullMarkerData? { + val input = + SceneMarkerCreateInput( + title = "", + seconds = position.toSeconds, + scene_id = sceneId, + primary_tag_id = primaryTagId, + tag_ids = Optional.absent(), + ) + val mutation = CreateMarkerMutation(input) + val result = executeMutation(mutation) + return result.data?.sceneMarkerCreate?.fullMarkerData + } + + suspend fun deleteMarker(id: String): Boolean { + val mutation = DeleteMarkerMutation(id) + val result = executeMutation(mutation) + return result.data?.sceneMarkerDestroy ?: false + } + + suspend fun setRating( + sceneId: String, + rating100: Int, + ): SceneUpdateMutation.SceneUpdate? { + Logger.v { "setRating sceneId=$sceneId, rating=$rating100" } + val mutation = + SceneUpdateMutation( + input = + SceneUpdateInput( + id = sceneId, + rating100 = Optional.present(rating100), + ), + ) + return executeMutation(mutation).data?.sceneUpdate + } + + suspend fun deleteScene( + sceneId: String, + deleteFiles: Boolean, + deleteGenerated: Boolean, + ): Boolean { + Logger.v { + "deleteScene: sceneId=$sceneId, deleteFiles=$deleteFiles, deleteGenerated=$deleteGenerated" + } + val mutation = + DeleteSceneMutation( + input = + SceneDestroyInput( + id = sceneId, + delete_file = Optional.present(deleteFiles), + delete_generated = Optional.present(deleteGenerated), + ), + ) + return executeMutation(mutation).data?.sceneDestroy == true + } + + suspend fun incrementImageOCounter(imageId: String): OCounter { + val mutation = ImageIncrementOMutation(imageId) + val result = executeMutation(mutation) + return OCounter(imageId, result.data!!.imageIncrementO) + } + + suspend fun decrementImageOCounter(imageId: String): OCounter { + val mutation = ImageDecrementOMutation(imageId) + val result = executeMutation(mutation) + return OCounter(imageId, result.data!!.imageDecrementO) + } + + suspend fun resetImageOCounter(imageId: String): OCounter { + val mutation = ImageResetOMutation(imageId) + val result = executeMutation(mutation) + return OCounter(imageId, result.data!!.imageResetO) + } + + suspend fun updateImage( + imageId: String, + studioId: String? = null, + performerIds: List? = null, + tagIds: List? = null, + galleryIds: List? = null, + rating100: Int? = null, + ): UpdateImageMutation.ImageUpdate? { + val mutation = + UpdateImageMutation( + ImageUpdateInput( + id = imageId, + studio_id = Optional.presentIfNotNull(studioId), + performer_ids = Optional.presentIfNotNull(performerIds), + tag_ids = Optional.presentIfNotNull(tagIds), + gallery_ids = Optional.presentIfNotNull(galleryIds), + rating100 = Optional.presentIfNotNull(rating100), + ), + ) + val result = executeMutation(mutation) + return result.data?.imageUpdate + } + + suspend fun updatePerformer( + performerId: String, + favorite: Boolean? = null, + rating100: Int? = null, + tagIds: List? = null, + ): PerformerData? { + val input = + PerformerUpdateInput( + id = performerId, + favorite = Optional.presentIfNotNull(favorite), + rating100 = Optional.presentIfNotNull(rating100), + tag_ids = Optional.presentIfNotNull(tagIds), + ) + return updatePerformer(input) + } + + suspend fun updatePerformer(input: PerformerUpdateInput): PerformerData? { + val mutation = UpdatePerformerMutation(input) + val result = executeMutation(mutation) + return result.data?.performerUpdate?.performerData + } + + suspend fun createTag(input: TagCreateInput): TagData? { + val mutation = CreateTagMutation(input) + val result = executeMutation(mutation) + return result.data?.tagCreate?.tagData + } + + suspend fun setTagFavorite( + tagId: String, + favorite: Boolean, + ): TagData? { + val input = + TagUpdateInput( + id = tagId, + favorite = Optional.present(favorite), + ) + val mutation = UpdateTagMutation(input) + return executeMutation(mutation).data?.tagUpdate?.tagData + } + + suspend fun createPerformer(input: PerformerCreateInput): PerformerData? { + val mutation = CreatePerformerMutation(input) + val result = executeMutation(mutation) + return result.data?.performerCreate?.performerData + } + + suspend fun createGroup(input: GroupCreateInput): GroupData { + val mutation = CreateGroupMutation(input) + val result = executeMutation(mutation) + return result.data?.groupCreate?.groupData!! + } + + suspend fun updateGallery( + galleryId: String, + rating100: Int, + ): GalleryData? { + val mutation = + UpdateGalleryMutation( + GalleryUpdateInput( + id = galleryId, + rating100 = Optional.present(rating100), + ), + ) + val result = executeMutation(mutation) + return result.data?.galleryUpdate?.galleryData + } + + suspend fun updateGallery( + galleryId: String, + rating100: Int? = null, + tagIds: List? = null, + performerIds: List? = null, + studioId: String? = null, + ): GalleryData? { + val mutation = + UpdateGalleryMutation( + GalleryUpdateInput( + id = galleryId, + rating100 = Optional.presentIfNotNull(rating100), + tag_ids = Optional.presentIfNotNull(tagIds), + performer_ids = Optional.presentIfNotNull(performerIds), + studio_id = Optional.presentIfNotNull(studioId), + ), + ) + val result = executeMutation(mutation) + return result.data?.galleryUpdate?.galleryData + } + + suspend fun installPackage( + type: PackageType, + input: PackageSpecInput, + ): String { + val mutation = InstallPackagesMutation(type, listOf(input)) + val result = executeMutation(mutation) + return result.data!!.installPackages + } + + suspend fun saveFilter(input: SaveFilterInput): SavedFilter { + val mutation = SaveFilterMutation(input) + return executeMutation(mutation).data!!.saveFilter.savedFilter + } + + suspend fun updateStudio( + studioId: String, + favorite: Boolean? = null, + rating100: Int? = null, + parentStudioId: String? = null, + tagIds: List? = null, + ): StudioData? { + val input = + StudioUpdateInput( + id = studioId, + favorite = Optional.presentIfNotNull(favorite), + rating100 = Optional.presentIfNotNull(rating100), + parent_id = Optional.presentIfNotNull(parentStudioId), + tag_ids = Optional.presentIfNotNull(tagIds), + ) + val mutation = UpdateStudioMutation(input) + return executeMutation(mutation).data?.studioUpdate?.studioData + } + + suspend fun createStudio(name: String): StudioData? { + val input = StudioCreateInput(name = name) + val mutation = CreateStudioMutation(input) + return executeMutation(mutation).data?.studioCreate?.studioData + } + + suspend fun updateGroup( + groupId: String, + rating100: Int? = null, + ): GroupData? { + val input = + GroupUpdateInput( + id = groupId, + rating100 = Optional.present(rating100), + ) + val mutation = UpdateGroupMutation(input) + return executeMutation(mutation).data?.groupUpdate?.groupData + } + + open class MutationException( + id: Int, + mutationName: String, + msg: String? = null, + cause: Exception? = null, + ) : ServerCommunicationException(id, mutationName, msg, cause) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt new file mode 100644 index 00000000..9ce4e63d --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt @@ -0,0 +1,522 @@ +package com.github.damontecres.stashapp.di.server + +import com.apollographql.apollo.ApolloCall +import com.apollographql.apollo.api.ApolloResponse +import com.apollographql.apollo.api.Operation +import com.apollographql.apollo.api.Optional +import com.apollographql.apollo.api.Query +import com.github.damontecres.stashapp.api.ConfigurationQuery +import com.github.damontecres.stashapp.api.FindGalleriesQuery +import com.github.damontecres.stashapp.api.FindGroupQuery +import com.github.damontecres.stashapp.api.FindGroupsQuery +import com.github.damontecres.stashapp.api.FindImageQuery +import com.github.damontecres.stashapp.api.FindImagesQuery +import com.github.damontecres.stashapp.api.FindJobQuery +import com.github.damontecres.stashapp.api.FindMarkersQuery +import com.github.damontecres.stashapp.api.FindPerformersQuery +import com.github.damontecres.stashapp.api.FindSavedFilterQuery +import com.github.damontecres.stashapp.api.FindSavedFiltersQuery +import com.github.damontecres.stashapp.api.FindScenesQuery +import com.github.damontecres.stashapp.api.FindSlimImagesQuery +import com.github.damontecres.stashapp.api.FindStudiosQuery +import com.github.damontecres.stashapp.api.FindTagsQuery +import com.github.damontecres.stashapp.api.GetExtraImageQuery +import com.github.damontecres.stashapp.api.GetGalleryQuery +import com.github.damontecres.stashapp.api.GetMarkerQuery +import com.github.damontecres.stashapp.api.GetPerformerQuery +import com.github.damontecres.stashapp.api.GetSceneQuery +import com.github.damontecres.stashapp.api.GetStudioQuery +import com.github.damontecres.stashapp.api.GetTagQuery +import com.github.damontecres.stashapp.api.GetVideoSceneQuery +import com.github.damontecres.stashapp.api.fragment.ExtraImageData +import com.github.damontecres.stashapp.api.fragment.FullMarkerData +import com.github.damontecres.stashapp.api.fragment.FullSceneData +import com.github.damontecres.stashapp.api.fragment.GalleryData +import com.github.damontecres.stashapp.api.fragment.GroupData +import com.github.damontecres.stashapp.api.fragment.ImageData +import com.github.damontecres.stashapp.api.fragment.MarkerData +import com.github.damontecres.stashapp.api.fragment.PerformerData +import com.github.damontecres.stashapp.api.fragment.SavedFilter +import com.github.damontecres.stashapp.api.fragment.SlimImageData +import com.github.damontecres.stashapp.api.fragment.SlimSceneData +import com.github.damontecres.stashapp.api.fragment.StashData +import com.github.damontecres.stashapp.api.fragment.StashJob +import com.github.damontecres.stashapp.api.fragment.StudioData +import com.github.damontecres.stashapp.api.fragment.TagData +import com.github.damontecres.stashapp.api.fragment.VideoSceneData +import com.github.damontecres.stashapp.api.type.CriterionModifier +import com.github.damontecres.stashapp.api.type.FindFilterType +import com.github.damontecres.stashapp.api.type.FindJobInput +import com.github.damontecres.stashapp.api.type.GalleryFilterType +import com.github.damontecres.stashapp.api.type.GroupFilterType +import com.github.damontecres.stashapp.api.type.ImageFilterType +import com.github.damontecres.stashapp.api.type.JobStatus +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.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 org.koin.core.annotation.Singleton +import timber.log.Timber +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +/** + * Handles making graphql queries to the server + */ +@Singleton +class QueryEngine( + api: StashApi, +) : StashEngine(api) { + private suspend fun executeQuery(query: ApolloCall): ApolloResponse = + withContext(Dispatchers.IO) { + val queryName = query.operation.name() + val id = getRequestId() + Timber.v("executeQuery $id $queryName") + + val response = query.execute() + if (response.data != null) { + Timber.v("executeQuery $id $queryName successful") + return@withContext response + } else if (response.exception != null) { + throw createException(id, queryName, response.exception!!) { msg, ex -> + QueryException(id, queryName, msg, ex) + } + } else { + val errorMessages = response.errors!!.joinToString("\n") { it.message } + Timber.e("Errors in $id $queryName: ${response.errors}") + throw QueryException(id, queryName, "Error in $queryName: $errorMessages") + } + } + + suspend fun executeQuery(query: Query): ApolloResponse = executeQuery(client.query(query)) + + suspend fun findScenes( + findFilter: FindFilterType? = null, + sceneFilter: SceneFilterType? = null, + ids: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindScenesQuery( + filter = updateFilter(findFilter, useRandom), + scene_filter = sceneFilter, + ids = ids, + ), + ) + val scenes = + executeQuery(query).data?.findScenes?.scenes?.map { + it.slimSceneData + } + return scenes.orEmpty() + } + + suspend fun getScene(sceneId: String): FullSceneData? { + val query = client.query(GetSceneQuery(id = sceneId)) + return executeQuery(query).data?.findScene?.fullSceneData + } + + suspend fun getVideoScene(sceneId: String): VideoSceneData? { + val query = client.query(GetVideoSceneQuery(id = sceneId)) + return executeQuery(query).data?.findScene?.videoSceneData + } + + suspend fun findPerformers( + findFilter: FindFilterType? = null, + performerFilter: PerformerFilterType? = null, + performerIds: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindPerformersQuery( + filter = updateFilter(findFilter, useRandom), + performer_filter = performerFilter, + ids = performerIds, + ), + ) + val performers = + executeQuery(query).data?.findPerformers?.performers?.map { + it.performerData + } + return performers.orEmpty() + } + + suspend fun getPerformer(performerId: String): PerformerData? { + val query = client.query(GetPerformerQuery(id = performerId)) + return executeQuery(query).data?.findPerformer?.performerData + } + + suspend fun findStudios( + findFilter: FindFilterType? = null, + studioFilter: StudioFilterType? = null, + studioIds: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindStudiosQuery( + filter = updateFilter(findFilter, useRandom), + studio_filter = studioFilter, + ids = studioIds, + ), + ) + val studios = + executeQuery(query).data?.findStudios?.studios?.map { + it.studioData + } + return studios.orEmpty() + } + + suspend fun getStudio(studioId: String): StudioData? { + val query = client.query(GetStudioQuery(id = studioId)) + return executeQuery(query).data?.findStudio?.studioData + } + + suspend fun findTags( + findFilter: FindFilterType? = null, + tagFilter: TagFilterType? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindTagsQuery( + filter = updateFilter(findFilter, useRandom), + tag_filter = tagFilter, + ids = null, + ), + ) + val tags = + executeQuery(query) + .data + ?.findTags + ?.tags + ?.map { it.tagData } + return tags.orEmpty() + } + + suspend fun getTags(tagIds: List): List { + if (tagIds.isEmpty()) { + return listOf() + } + val query = + client.query( + FindTagsQuery( + filter = null, + tag_filter = null, + ids = tagIds, + ), + ) + val tags = + executeQuery(query) + .data + ?.findTags + ?.tags + ?.map { it.tagData } + return tags.orEmpty() + } + + suspend fun getTag(tagId: String): TagData? { + val query = client.query(GetTagQuery(id = tagId)) + return executeQuery(query).data?.findTag?.tagData + } + + suspend fun findGroups( + findFilter: FindFilterType? = null, + groupFilter: GroupFilterType? = null, + groupIds: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindGroupsQuery( + filter = updateFilter(findFilter, useRandom), + group_filter = groupFilter, + ids = groupIds, + ), + ) + val tags = + executeQuery(query) + .data + ?.findGroups + ?.groups + ?.map { it.groupData } + return tags.orEmpty() + } + + suspend fun getGroup(groupId: String): GroupData? { + val query = client.query(FindGroupQuery(groupId)) + return executeQuery(query).data?.findGroup?.groupData + } + + suspend fun findMarkers( + findFilter: FindFilterType? = null, + markerFilter: SceneMarkerFilterType? = null, + markerIds: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindMarkersQuery( + filter = updateFilter(findFilter, useRandom), + scene_marker_filter = markerFilter, + ids = markerIds, + ), + ) + return executeQuery(query) + .data + ?.findSceneMarkers + ?.scene_markers + ?.map { it.markerData } + .orEmpty() + } + + suspend fun getMarker(markerId: String): FullMarkerData? { + val query = client.query(GetMarkerQuery(listOf(markerId))) + return executeQuery(query) + .data + ?.findSceneMarkers + ?.scene_markers + ?.firstOrNull() + ?.fullMarkerData + } + + suspend fun findMarkersInScene(sceneId: String): List = + findMarkers( + markerFilter = + SceneMarkerFilterType( + scenes = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(sceneId)), + modifier = CriterionModifier.INCLUDES_ALL, + ), + ), + ), + ).sortedBy { it.seconds } + + suspend fun findImages( + findFilter: FindFilterType? = null, + imageFilter: ImageFilterType? = null, + ids: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindImagesQuery( + filter = updateFilter(findFilter, useRandom), + image_filter = imageFilter, + ids = ids, + ), + ) + return executeQuery(query) + .data + ?.findImages + ?.images + ?.map { it.imageData } + .orEmpty() + } + + suspend fun findSlimImages( + findFilter: FindFilterType? = null, + imageFilter: ImageFilterType? = null, + ids: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindSlimImagesQuery( + filter = updateFilter(findFilter, useRandom), + image_filter = imageFilter, + ids = ids, + ), + ) + return executeQuery(query) + .data + ?.findImages + ?.images + ?.map { it.slimImageData } + .orEmpty() + } + + suspend fun getImage(imageId: String): ImageData? { + val query = client.query(FindImageQuery(imageId)) + return executeQuery(query).data?.findImage?.imageData + } + + suspend fun getImageExtra(imageId: String): ExtraImageData? { + val query = client.query(GetExtraImageQuery(imageId)) + return executeQuery(query).data?.findImage?.extraImageData + } + + suspend fun findGalleries( + findFilter: FindFilterType? = null, + galleryFilter: GalleryFilterType? = null, + galleryIds: List? = null, + useRandom: Boolean = true, + ): List { + val query = + client.query( + FindGalleriesQuery( + updateFilter(findFilter, useRandom), + galleryFilter, + ids = galleryIds, + ), + ) + return executeQuery(query) + .data + ?.findGalleries + ?.galleries + ?.map { it.galleryData } + .orEmpty() + } + + suspend fun getGalleries(galleryIds: List): List = + if (galleryIds.isEmpty()) { + listOf() + } else { + val query = client.query(FindGalleriesQuery(null, null, galleryIds)) + executeQuery(query) + .data + ?.findGalleries + ?.galleries + ?.map { it.galleryData } + .orEmpty() + } + + suspend fun getGallery(galleryId: String): GalleryData? { + val query = client.query(GetGalleryQuery(id = galleryId)) + return executeQuery(query).data?.findGallery?.galleryData + } + + /** + * Search for a type of data with the given query. Users will need to cast the returned List. + */ + suspend fun find( + type: DataType, + findFilter: FindFilterType, + useRandom: Boolean = true, + ): List = + when (type) { + DataType.SCENE -> findScenes(findFilter, useRandom = useRandom) + DataType.PERFORMER -> findPerformers(findFilter, useRandom = useRandom) + DataType.TAG -> findTags(findFilter, useRandom = useRandom) + DataType.STUDIO -> findStudios(findFilter, useRandom = useRandom) + DataType.GROUP -> findGroups(findFilter, useRandom = useRandom) + 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. + */ + suspend fun getByIds( + type: DataType, + ids: List, + ): List { + if (ids.isEmpty()) { + return emptyList() + } + return when (type) { + DataType.SCENE -> findScenes(ids = ids) + DataType.PERFORMER -> findPerformers(performerIds = ids) + DataType.TAG -> getTags(ids) + DataType.STUDIO -> findStudios(studioIds = ids) + 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 + } as List + } + + suspend fun getSavedFilter(filterId: String): SavedFilter? { + val query = FindSavedFilterQuery(filterId) + return executeQuery(query).data?.findSavedFilter?.savedFilter + } + + suspend fun getSavedFilters(dataType: DataType): List { + val query = FindSavedFiltersQuery(dataType.filterMode) + return executeQuery(query) + .data + ?.findSavedFilters + ?.map { it.savedFilter } + .orEmpty() + } + + suspend fun getServerConfiguration(): ConfigurationQuery.Data { + val query = ConfigurationQuery() + return executeQuery(query).data!! + } + + suspend fun getJob(jobId: String): StashJob? { + val query = FindJobQuery(FindJobInput(jobId)) + return executeQuery(query).data?.findJob?.stashJob + } + + suspend fun waitForJob( + jobId: String, + delay: Duration = 1.toDuration(DurationUnit.SECONDS), + callback: ((StashJob) -> Unit)? = null, + ): JobResult { + var job = getJob(jobId) ?: return JobResult.NotFound + while (job.status !in + setOf( + JobStatus.FINISHED, + JobStatus.FAILED, + JobStatus.CANCELLED, + ) + ) { + delay(delay) + job = getJob(jobId) ?: return JobResult.NotFound + callback?.invoke(job) + } + return if (job.status == JobStatus.FAILED) { + JobResult.Failure(job.error) + } else { + JobResult.Success + } + } + + /** + * Updates a FindFilterType if needed + * + * Handles updating the random sort if requested + */ + fun updateFilter( + filter: FindFilterType?, + useRandom: Boolean = true, + ): FindFilterType? = + if (filter != null) { + if (useRandom && filter.sort.getOrNull()?.startsWith("random") == true) { + Timber.v("Updating random filter") + filter.copy(sort = Optional.present("random_" + getRandomSort())) + } else { + filter + } + } else { + null + } + + companion object { + private const val TAG = "QueryEngine" + } + + open class QueryException( + id: Int, + queryName: String, + msg: String? = null, + cause: Exception? = null, + ) : ServerCommunicationException(id, queryName, msg, cause) + + class StashNotConfiguredException : RuntimeException() +} 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 00000000..878779be --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerRepository.kt @@ -0,0 +1,5 @@ +package com.github.damontecres.stashapp.di.server + +class ServerRepository { + val currentServer: StashServer = StashServer.UNSET +} 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 00000000..074da8d4 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashApi.kt @@ -0,0 +1,179 @@ +package com.github.damontecres.stashapp.di.server + +import android.annotation.SuppressLint +import android.content.Context +import androidx.datastore.core.DataStore +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.AuthHttpClient +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 kotlinx.coroutines.flow.first +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:AuthHttpClient private val httpClient: OkHttpClient, + private val preferences: DataStore, +) { + @OptIn(ApolloExperimental::class) + var server: StashServer = StashServer.UNSET + private set + + lateinit var apolloClient: ApolloClient + private set + + @OptIn(ApolloExperimental::class) + suspend fun changeServer(server: StashServer) { + Timber.i("Switching server to %s", server.url) + val client = createOkHttpClient() + val apolloClient = + ApolloClient + .Builder() + .serverUrl(server.url) + .httpEngine(DefaultHttpEngine(client)) + .subscriptionNetworkTransport( + WebSocketNetworkTransport + .Builder() + .serverUrl(server.url) + .wsProtocol(GraphQLWsProtocol()) + .webSocketEngine(WebSocketEngine(client)) + .build(), + ).build() + this.apolloClient = apolloClient + this.server = server + } + + private suspend fun createOkHttpClient(): OkHttpClient { + val preferences = preferences.data.first() + 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.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 { + } +} + +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/di/server/StashEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt new file mode 100644 index 00000000..7d6ae23e --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt @@ -0,0 +1,64 @@ +package com.github.damontecres.stashapp.di.server + +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 api: StashApi, +) { + protected val client get() = api.apolloClient + private var requestId: Int = 0 + protected val mutex = Mutex() + + protected suspend fun getRequestId() = mutex.withLock { requestId++ } + + protected fun createException( + id: Int, + operationName: String, + ex: Exception, + rethrow: (String, Exception) -> T, + ): T { + val causeMsg = + if (ex.message.isNotNullOrBlank()) { + ex.message + } else { + ex.cause?.message + } + return when (ex) { + is ApolloNetworkException -> { + Timber.e(ex, "Network error in $id $operationName") + rethrow("Network error ($operationName): $causeMsg", ex) + } + + is ApolloHttpException -> { + Timber.e(ex, "HTTP ${ex.statusCode} error in $id $operationName") + rethrow("HTTP ${ex.statusCode} ($operationName): $causeMsg", ex) + } + + is ApolloException -> { + Timber.e(ex, "ApolloException in $id $operationName") + rethrow("Error with server ($operationName): $causeMsg", ex) + } + + else -> { + Timber.e(ex, "Exception in $id $operationName") + rethrow("Error with $operationName: $causeMsg", ex) + } + } + } + + open class ServerCommunicationException( + val operationId: Int, + val operationName: String, + message: String? = null, + cause: Exception? = null, + ) : Exception(message, cause) +} 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 00000000..9e500084 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashServer.kt @@ -0,0 +1,48 @@ +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) } + + 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/di/server/SubscriptionEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt new file mode 100644 index 00000000..0da03734 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt @@ -0,0 +1,66 @@ +package com.github.damontecres.stashapp.di.server + +import android.util.Log +import com.apollographql.apollo.api.Subscription +import com.github.damontecres.stashapp.api.JobProgressSubscription +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.concurrent.atomic.AtomicInteger + +/** + * Engine for handling graphql subscriptions + * + * @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 + */ +class SubscriptionEngine( + api: StashApi, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, + private val callbackDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : StashEngine(api) { + private suspend fun executeSubscription( + subscription: Subscription, + consumer: (D) -> Unit, + ) = withContext(ioDispatcher) { + val name = subscription.name() + val id = OPERATION_ID.getAndIncrement() + + client.subscription(subscription).toFlow().collect { response -> + if (response.data != null) { + Log.v(TAG, "executeSubscription $id $name response received") + withContext(callbackDispatcher) { + consumer.invoke(response.data!!) + } + } else if (response.exception != null) { + throw createException(id, name, response.exception!!) { msg, ex -> + SubscriptionException(id, name, msg, ex) + } + } else { + val errorMessages = response.errors!!.joinToString("\n") { it.message } + Log.e(TAG, "Errors in $id $name: ${response.errors}") + throw SubscriptionException(id, name, "Error in $name: $errorMessages") + } + } + Log.v(TAG, "Completed subscription $id $name") + } + + suspend fun subscribeToJobs(consumer: (JobProgressSubscription.Data) -> Unit) { + val subscription = JobProgressSubscription() + executeSubscription(subscription, consumer) + } + + companion object { + private const val TAG = "SubscriptionEngine" + private val OPERATION_ID = AtomicInteger(0) + } + + open class SubscriptionException( + id: Int, + mutationName: String, + msg: String? = null, + cause: Exception? = null, + ) : ServerCommunicationException(id, mutationName, msg, cause) +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt index de0f9ff6..c4c9d109 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/Constants.kt @@ -108,7 +108,7 @@ object Constants { */ const val STASH_API_HEADER = "ApiKey" const val TAG = "Constants" - private const val OK_HTTP_CACHE_DIR = "okhttpcache" + const val OK_HTTP_CACHE_DIR = "okhttpcache" fun getNetworkCache(context: Context): Cache { val cacheSize = diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ff783311..c3c18081 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,7 @@ compose-wheel-picker = "1.0.0-rc02" constraintlayout = "2.2.1" datastore = "1.2.1" desugar_jdk_libs = "2.1.5" +kermit = "2.1.0" markwon-core = "4.6.2" core-ktx = "1.18.0" glide = "5.0.7" @@ -53,6 +54,8 @@ viewpump = "4.0.15" zoomlayout = "1.9.0" slf4j2Timber = "1.2" timber = "5.0.1" +koin-bom = "4.2.1" +koin-plugin = "1.0.0-RC2" wholphin-extensions = "0.2.0" [libraries] @@ -103,6 +106,7 @@ coil-network-cachecontrol = { module = "io.coil-kt.coil3:coil-network-cache-cont coil-svg = { module = "io.coil-kt.coil3:coil-svg", version.ref = "coil-compose" } compose-wheel-picker = { module = "com.github.zj565061763:compose-wheel-picker", version.ref = "compose-wheel-picker" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } +kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon-core" } glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } glide-okhttp3-integration = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" } @@ -143,6 +147,14 @@ androidx-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } slf4j2-timber = { module = "uk.kulikov:slf4j2-timber", version.ref = "slf4j2Timber" } +koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } +koin-core = { module = "io.insert-koin:koin-core" } +koin-annotations = { module = "io.insert-koin:koin-annotations" } +koin-android = { module = "io.insert-koin:koin-android" } +koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" } +koin-compose = { module = "io.insert-koin:koin-compose" } +koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } + wholphin-extensions-mpv = { module = "com.github.damontecres.wholphin.mpv:wholphin-mpv", version.ref = "wholphin-extensions" } wholphin-extensions-ffmpeg = { module = "com.github.damontecres.wholphin.media3:decoder-ffmpeg", version.ref = "wholphin-extensions" } wholphin-extensions-av1 = { module = "com.github.damontecres.wholphin.media3:decoder-av1", version.ref = "wholphin-extensions" } @@ -156,3 +168,4 @@ kotlin-plugin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization" ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } room = { id = "androidx.room", version.ref = "room" } protobuf = { id = "com.google.protobuf", version.ref = "protobuf" } +koin-compiler = { id = "io.insert-koin.compiler.plugin", version.ref = "koin-plugin" } From 141f604fb9a63b8122c77cb0d4c16fa3da67bf0f Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 15:39:14 -0400 Subject: [PATCH 02/25] Add server prefs & initial compose conversions --- app/build.gradle.kts | 6 + app/src/main/AndroidManifest.xml | 31 +- .../damontecres/stashapp/MainActivity.kt | 23 ++ .../damontecres/stashapp/di/AppModule.kt | 8 +- .../stashapp/di/server/MutationEngine.kt | 8 +- .../stashapp/di/server/QueryEngine.kt | 9 +- .../stashapp/di/server/ServerPreferences.kt | 378 ++++++++++++++++++ .../stashapp/di/server/ServerRepository.kt | 156 +++++++- .../stashapp/di/server/StashEngine.kt | 3 +- .../stashapp/di/server/SubscriptionEngine.kt | 2 + .../stashapp/di/services/NavigationManager.kt | 86 ++++ .../stashapp/di/services/ServerLogger.kt | 35 ++ .../stashapp/navigation/Destination.kt | 4 +- .../stashapp/suppliers/StashPagingSource.kt | 1 - .../components/scene/SceneDetailsViewModel.kt | 88 ++-- .../stashapp/ui/nav/DestinationContent.kt | 2 - .../damontecres/stashapp/ui/pages/MainPage.kt | 134 ++++--- .../stashapp/ui/pages/SceneDetailsPage.kt | 45 +-- .../stashapp/ui/util/ScreenSize.kt | 35 +- gradle/libs.versions.toml | 15 +- 20 files changed, 890 insertions(+), 179 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/ServerPreferences.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/services/NavigationManager.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/services/ServerLogger.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f485a77..7e9014f0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -234,10 +234,16 @@ 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.androidx.navigation3.runtime) + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.androidx.material3.adaptive.navigation3) implementation(libs.glide) implementation(libs.glide.okhttp3.integration) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f2f9857b..359786e5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,24 +27,29 @@ 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 +62,13 @@ - - - - - - - - - - + + + + + + + + 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 00000000..719b00cc --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt @@ -0,0 +1,23 @@ +package com.github.damontecres.stashapp + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import com.github.damontecres.stashapp.di.services.NavigationManager +import com.github.damontecres.stashapp.util.preferences +import org.koin.android.ext.android.inject + +class MainActivity : AppCompatActivity() { + private val navigationManager: NavigationManager by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + val preferences by this@MainActivity.preferences.data.collectAsState(null) + preferences?.let { + } + } + } +} 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 index 37a58875..743a543a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt @@ -2,12 +2,15 @@ package com.github.damontecres.stashapp.di import android.content.Context import android.os.Build +import androidx.datastore.core.DataStore import co.touchlab.kermit.Logger import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.di.server.ServerRepository +import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.util.Constants 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 @@ -51,7 +54,7 @@ fun provideAuthHttpClient( httpClient .newBuilder() .addInterceptor { - val server = serverRepository.currentServer + val server = serverRepository.currentServer.value.server val request = if (server.apiKey != null) { val isStashUrl = @@ -94,3 +97,6 @@ fun createUserAgent(context: Context): String { ).joinNotNullOrBlank("; ") return "$appName/$versionStr ($comments) ($device)" } + +@Single +fun provideDataStore(context: Context): DataStore = context.preferences diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt index 73f01b47..3302b559 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt @@ -1,4 +1,4 @@ -package com.github.damontecres.stashappapp.di.server +package com.github.damontecres.stashapp.di.server import co.touchlab.kermit.Logger import com.apollographql.apollo.api.ApolloResponse @@ -55,17 +55,15 @@ 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.di.server.StashApi -import com.github.damontecres.stashapp.di.server.StashEngine import com.github.damontecres.stashapp.util.toSeconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.koin.core.annotation.Singleton +import org.koin.core.annotation.Single /** * Class for sending graphql mutations */ -@Singleton +@Single class MutationEngine( api: StashApi, ) : StashEngine(api) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt index 9ce4e63d..edb502b6 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/QueryEngine.kt @@ -63,7 +63,7 @@ import com.github.damontecres.stashapp.util.getRandomSort import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext -import org.koin.core.annotation.Singleton +import org.koin.core.annotation.Single import timber.log.Timber import kotlin.time.Duration import kotlin.time.DurationUnit @@ -72,10 +72,15 @@ import kotlin.time.toDuration /** * Handles making graphql queries to the server */ -@Singleton +@Single class QueryEngine( api: StashApi, ) : StashEngine(api) { + // TODO remove this + fun asUtilQueryEngine() = + com.github.damontecres.stashapp.util + .QueryEngine(api.apolloClient, null) + private suspend fun executeQuery(query: ApolloCall): ApolloResponse = withContext(Dispatchers.IO) { val queryName = query.operation.name() 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 00000000..7e25cec4 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/ServerPreferences.kt @@ -0,0 +1,378 @@ +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.ServerPreferences.Companion.PREF_MINIMUM_PLAY_PERCENT +import com.github.damontecres.stashapp.util.ServerPreferences.Companion.PREF_TRACK_ACTIVITY +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.CompanionPlugin +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 == CompanionPlugin.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( + PREF_TRACK_ACTIVITY, + )?.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(PREF_MINIMUM_PLAY_PERCENT)?.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 index 878779be..dcd2a7f6 100644 --- 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 @@ -1,5 +1,157 @@ package com.github.damontecres.stashapp.di.server -class ServerRepository { - val currentServer: StashServer = StashServer.UNSET +import android.content.Context +import android.content.SharedPreferences +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import com.github.damontecres.stashapp.SettingsFragment +import com.github.damontecres.stashapp.util.isNotNullOrBlank +import kotlinx.coroutines.flow.MutableStateFlow +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 queryEngine: QueryEngine, +) { + 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(SettingsFragment.PREF_STASH_URL, null) + val apiKey = manager.getString(SettingsFragment.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(SettingsFragment.PREF_STASH_URL, null) + val apiKey = manager.getString(SettingsFragment.PREF_STASH_API_KEY, null) + return if (url.isNotNullOrBlank()) { + servers.getOrPut(url) { StashServer(url, apiKey) } + } else { + null + } + } + + suspend fun setCurrentStashServer(server: StashServer) { + val manager = PreferenceManager.getDefaultSharedPreferences(context) + manager.edit(true) { + putString(SettingsFragment.PREF_STASH_URL, server.url) + putString(SettingsFragment.PREF_STASH_API_KEY, server.apiKey) + } + } + + 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, + otherSettings: ((SharedPreferences.Editor) -> Unit)? = null, + ) { + 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(SettingsFragment.PREF_STASH_URL, newServer.url) + putString(SettingsFragment.PREF_STASH_API_KEY, newServer.apiKey) + if (otherSettings != null) { + otherSettings(this) + } + } + api.changeServer(newServer) + val config = queryEngine.getServerConfiguration() + val serverPreferences = ServerPreferences.createServerPreferences(config) + currentServer.update { + CurrentServer(newServer, serverPreferences) + } + } + + suspend fun updateServerPreferences() { + val config = queryEngine.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_" + } +} + +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/StashEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt index 7d6ae23e..b298109b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashEngine.kt @@ -12,7 +12,8 @@ import timber.log.Timber * Super class for "engines" that interact with the server */ abstract class StashEngine( - protected val api: StashApi, + // TODO protected + val api: StashApi, ) { protected val client get() = api.apolloClient private var requestId: Int = 0 diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt index 0da03734..9cfef88a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/SubscriptionEngine.kt @@ -6,6 +6,7 @@ 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 /** @@ -16,6 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger * @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( api: StashApi, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, 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 00000000..4ec67380 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/NavigationManager.kt @@ -0,0 +1,86 @@ +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 reloadHome() { + goToMain() + // TODO +// 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/ServerLogger.kt b/app/src/main/java/com/github/damontecres/stashapp/di/services/ServerLogger.kt new file mode 100644 index 00000000..4974ee5e --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/ServerLogger.kt @@ -0,0 +1,35 @@ +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 kotlinx.coroutines.flow.first +import org.koin.core.annotation.Single + +@Single +class ServerLogger( + private val preferences: DataStore, + private val serverRepository: ServerRepository, +) { + suspend fun logException( + exception: Throwable, + message: String? = null, + ) { + if (preferences.data + .first() + .advancedPreferences.logErrorsToServer + ) { + if (serverRepository.currentServer.value.serverPreferences.companionPluginInstalled) { + // TODO + // CompanionPlugin.sendLogMessage(server, message, 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/navigation/Destination.kt b/app/src/main/java/com/github/damontecres/stashapp/navigation/Destination.kt index 1863a510..119911bd 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/navigation/Destination.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/navigation/Destination.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.os.Parcel import android.os.Parcelable import android.os.Parcelable.Creator +import androidx.navigation3.runtime.NavKey import com.github.damontecres.stashapp.PreferenceScreenOption import com.github.damontecres.stashapp.api.fragment.ExtraImageData import com.github.damontecres.stashapp.api.fragment.FullMarkerData @@ -40,7 +41,8 @@ import java.util.concurrent.atomic.AtomicLong sealed class Destination( val fullScreen: Boolean = false, val fullScreenTouch: Boolean = fullScreen, -) : Parcelable { +) : Parcelable, + NavKey { protected val destId = counter.getAndIncrement() val fragmentTag = "${this::class.simpleName}_$destId" diff --git a/app/src/main/java/com/github/damontecres/stashapp/suppliers/StashPagingSource.kt b/app/src/main/java/com/github/damontecres/stashapp/suppliers/StashPagingSource.kt index 36d65851..40aa1992 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/suppliers/StashPagingSource.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/suppliers/StashPagingSource.kt @@ -6,7 +6,6 @@ import com.apollographql.apollo.api.Query import com.github.damontecres.stashapp.api.fragment.StashData import com.github.damontecres.stashapp.api.type.FindFilterType import com.github.damontecres.stashapp.data.DataType -import com.github.damontecres.stashapp.suppliers.StashPagingSource.DataTransform import com.github.damontecres.stashapp.util.QueryEngine import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt index 823e8ad5..80051113 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt @@ -1,12 +1,11 @@ package com.github.damontecres.stashapp.ui.components.scene +import android.app.Application +import androidx.datastore.core.DataStore import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory +import co.touchlab.kermit.Logger import com.apollographql.apollo.api.Query import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.fragment.FullSceneData @@ -18,6 +17,11 @@ import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.api.fragment.StudioData import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.OCounter +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.ServerLogger +import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.suppliers.DataSupplierFactory import com.github.damontecres.stashapp.suppliers.StashPagingSource import com.github.damontecres.stashapp.ui.showAddGallery @@ -26,30 +30,46 @@ import com.github.damontecres.stashapp.ui.showAddMarker import com.github.damontecres.stashapp.ui.showAddPerf import com.github.damontecres.stashapp.ui.showAddTag import com.github.damontecres.stashapp.ui.showSetStudio -import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -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.asMarkerData import com.github.damontecres.stashapp.util.createSceneSuggestionFilter +import com.github.damontecres.stashapp.util.launchIO import com.github.damontecres.stashapp.util.showSetRatingToast import com.github.damontecres.stashapp.util.toLongMilliseconds +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel +import kotlin.coroutines.CoroutineContext +@KoinViewModel class SceneDetailsViewModel( - val server: StashServer, - val sceneId: String, - val pageSize: Int, + private val context: Application, + private val serverLogger: ServerLogger, + private val queryEngine: QueryEngine, + private val mutationEngine: MutationEngine, + private val serverRepository: ServerRepository, + private val preferences: DataStore, + @InjectedParam val sceneId: String, ) : ViewModel() { - private val queryEngine = QueryEngine(server) - private val mutationEngine = MutationEngine(server) private val exceptionHandler = - LoggingCoroutineExceptionHandler( - server, - viewModelScope, - toastMessage = "Error updating scene", - ) + object : CoroutineExceptionHandler { + override val key: CoroutineContext.Key<*> + get() = CoroutineExceptionHandler + + override fun handleException( + context: CoroutineContext, + exception: Throwable, + ) { + Logger.e(exception) { "Exception" } + viewModelScope.launchIO { + serverLogger.logException(exception, null) + } + } + } + + val currentServer get() = serverRepository.currentServer private var scene: FullSceneData? = null @@ -94,11 +114,7 @@ class SceneDetailsViewModel( } } catch (ex: Exception) { loadingState.value = SceneLoadingState.Error - LoggingCoroutineExceptionHandler( - server, - viewModelScope, - toastMessage = "Error loading scene", - ).handleException(ex) + serverLogger.logException(ex) } } return this @@ -111,13 +127,18 @@ class SceneDetailsViewModel( val filterArgs = createSceneSuggestionFilter(it) if (filterArgs != null) { val supplier = - DataSupplierFactory(server.version) + DataSupplierFactory(serverRepository.currentServerVersion) .create(filterArgs) suggestions.value = StashPagingSource( - queryEngine, + queryEngine.asUtilQueryEngine(), supplier, - ).fetchPage(1, pageSize) + ).fetchPage( + 1, + preferences.data + .first() + .searchPreferences.maxResults, + ) } } } @@ -320,21 +341,6 @@ class SceneDetailsViewModel( } } } - - companion object { - val SERVER_KEY = object : CreationExtras.Key {} - val SCENE_ID_KEY = object : CreationExtras.Key {} - val PAGE_SIZE_KEY = object : CreationExtras.Key {} - val Factory: ViewModelProvider.Factory = - viewModelFactory { - initializer { - val server = this[SERVER_KEY]!! - val sceneId = this[SCENE_ID_KEY]!! - val pageSize = this[PAGE_SIZE_KEY]!! - SceneDetailsViewModel(server, sceneId, pageSize) - } - } - } } sealed class SceneLoadingState { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt index 845d382f..ec346ce9 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt @@ -193,7 +193,6 @@ fun DestinationContent( Destination.Main -> { MainPage( - server = server, uiConfig = composeUiConfig, itemOnClick = itemOnClick, longClicker = longClicker, @@ -241,7 +240,6 @@ fun DestinationContent( DataType.SCENE -> { SceneDetailsPage( modifier = modifier, - server = server, navigationManager = navManager, sceneId = destination.id, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt index 92a602b8..1f0eb5b4 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt @@ -1,6 +1,6 @@ package com.github.damontecres.stashapp.ui.pages -import android.content.Context +import android.app.Application import android.util.Log import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement @@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf @@ -42,14 +43,15 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.datastore.core.DataStore import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text +import co.touchlab.kermit.Logger import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.transitionFactory @@ -63,9 +65,13 @@ import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.api.fragment.StudioData import com.github.damontecres.stashapp.api.fragment.TagData +import com.github.damontecres.stashapp.di.server.CurrentServer +import com.github.damontecres.stashapp.di.server.QueryEngine +import com.github.damontecres.stashapp.di.server.ServerPreferences +import com.github.damontecres.stashapp.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.navigation.FilterAndPosition import com.github.damontecres.stashapp.proto.StashPreferences -import com.github.damontecres.stashapp.proto.UpdatePreferences import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.LocalGlobalContext import com.github.damontecres.stashapp.ui.cards.StashCard @@ -79,83 +85,91 @@ import com.github.damontecres.stashapp.ui.components.main.MainPageHeader import com.github.damontecres.stashapp.ui.isPlayKeyUp import com.github.damontecres.stashapp.ui.tryRequestFocus import com.github.damontecres.stashapp.ui.util.CrossFadeFactory -import com.github.damontecres.stashapp.ui.util.OneTimeLaunchedEffect import com.github.damontecres.stashapp.ui.util.getPlayDestinationForItem import com.github.damontecres.stashapp.ui.util.ifElse import com.github.damontecres.stashapp.util.FilterParser import com.github.damontecres.stashapp.util.FrontPageParser -import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -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.UpdateChecker -import com.github.damontecres.stashapp.util.getCaseInsensitive import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.launchDefault import com.github.damontecres.stashapp.util.launchIO import com.github.damontecres.stashapp.views.formatBytes import com.github.damontecres.stashapp.views.formatNumber +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import org.koin.compose.viewmodel.koinViewModel +import org.koin.core.annotation.KoinViewModel import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds private const val TAG = "MainPage" -class MainPageViewModel : ViewModel() { - private lateinit var server: StashServer - +@KoinViewModel +class MainPageViewModel( + private val context: Application, + private val serverLogger: ServerLogger, + private val queryEngine: QueryEngine, + private val serverRepository: ServerRepository, + private val preferences: DataStore, +) : ViewModel() { val frontPageRows = mutableStateListOf() private val _serverStats = MutableLiveData() val serverStats: LiveData = _serverStats - fun init( - context: Context, - server: StashServer, - prefs: StashPreferences, - ) { - this.server = server - viewModelScope.launchDefault(LoggingCoroutineExceptionHandler(server, viewModelScope)) { - val queryEngine = QueryEngine(server) - val filterParser = FilterParser(server.version) - val frontPageContent = - server.serverPreferences.uiConfiguration?.getCaseInsensitive("frontPageContent") as List>? - if (frontPageContent != null) { - Log.d(TAG, "${frontPageContent.size} front page rows") - val frontPageParser = - FrontPageParser( - context, - queryEngine, - filterParser, - prefs.searchPreferences.maxResults, - ) - val jobs = frontPageParser.parse(frontPageContent) + val currentServer get() = serverRepository.currentServer - jobs.forEach { job -> - try { - job.await().let { row -> - if (row is FrontPageParser.FrontPageRow.Success) { - frontPageRows.add(row) + init { + viewModelScope.launchDefault { + try { + val current = serverRepository.currentServer.value + val filterParser = FilterParser(current.serverPreferences.version) + val frontPageContent = current.serverPreferences.frontPageContent + if (frontPageContent.isNotEmpty()) { + Log.d(TAG, "${frontPageContent.size} front page rows") + val pageSize = + preferences.data + .first() + .searchPreferences.maxResults + val frontPageParser = + FrontPageParser( + context, + queryEngine.asUtilQueryEngine(), + filterParser, + pageSize, + ) + val jobs = frontPageParser.parse(frontPageContent) + + jobs.forEach { job -> + try { + job.await().let { row -> + if (row is FrontPageParser.FrontPageRow.Success) { + frontPageRows.add(row) + } } + } catch (ex: Exception) { + Log.e(TAG, "Error fetching row data", ex) } - } catch (ex: Exception) { - Log.e(TAG, "Error fetching row data", ex) } + } else { + Logger.w { "No front page content!" } } + } catch (ex: Exception) { + Logger.e(ex) { "" } + serverLogger.logException(ex, null) } } } - fun checkForUpdate( - context: Context, - prefs: UpdatePreferences, - ) { - if (prefs.checkForUpdates) { - viewModelScope.launchIO { + fun checkForUpdate() { + viewModelScope.launchIO { + val updatePrefs = preferences.data.first().updatePreferences + if (updatePrefs.checkForUpdates) { try { UpdateChecker.maybeShowUpdateToast( context, - prefs.updateUrl, + updatePrefs.updateUrl, false, ) } catch (ex: Exception) { @@ -167,7 +181,6 @@ class MainPageViewModel : ViewModel() { fun updateStatistics() { viewModelScope.launch(StashCoroutineExceptionHandler()) { - val queryEngine = QueryEngine(server) _serverStats.value = queryEngine.executeQuery(StatisticsQuery()).data?.stats } } @@ -175,24 +188,21 @@ class MainPageViewModel : ViewModel() { @Composable fun MainPage( - server: StashServer, uiConfig: ComposeUiConfig, itemOnClick: ItemOnClicker, longClicker: LongClicker, modifier: Modifier = Modifier, - viewModel: MainPageViewModel = viewModel(), + viewModel: MainPageViewModel = koinViewModel(), ) { val context = LocalContext.current - OneTimeLaunchedEffect { - viewModel.init(context, server, uiConfig.preferences) - } val frontPageRows = viewModel.frontPageRows // .observeAsState(listOf()) val serverStats by viewModel.serverStats.observeAsState() + val currentServer by viewModel.currentServer.collectAsState() val focusRequester = remember { FocusRequester() } LaunchedEffect(Unit) { - viewModel.checkForUpdate(context, uiConfig.preferences.updatePreferences) + viewModel.checkForUpdate() viewModel.updateStatistics() } if (frontPageRows.isEmpty()) { @@ -205,12 +215,12 @@ fun MainPage( ) } } else { - LaunchedEffect(server, frontPageRows) { + LaunchedEffect(currentServer, frontPageRows) { focusRequester.tryRequestFocus() } HomePage( modifier = modifier.focusRequester(focusRequester), - server = server, + server = currentServer, serverStats = serverStats, uiConfig = uiConfig, rows = frontPageRows, @@ -223,7 +233,7 @@ fun MainPage( @OptIn(ExperimentalComposeUiApi::class) @Composable fun HomePage( - server: StashServer, + server: CurrentServer, serverStats: StatisticsQuery.Stats?, uiConfig: ComposeUiConfig, rows: List, @@ -353,7 +363,7 @@ fun HomePage( } item { ServerStatsRow( - server = server, + serverPreferences = server.serverPreferences, serverStats = serverStats, modifier = Modifier @@ -480,7 +490,7 @@ fun HomePageRow( @Composable fun ServerStatsRow( - server: StashServer, + serverPreferences: ServerPreferences, serverStats: StatisticsQuery.Stats?, modifier: Modifier = Modifier, ) { @@ -491,7 +501,7 @@ fun ServerStatsRow( ) { TitleValueText( stringResource(R.string.stashapp_scenes), - formatNumber(stats.scene_count, server.serverPreferences.abbreviateCounters), + formatNumber(stats.scene_count, serverPreferences.abbreviateCounters), ) TitleValueText( stringResource(R.string.stashapp_stats_scenes_size), @@ -499,7 +509,7 @@ fun ServerStatsRow( ) TitleValueText( stringResource(R.string.stashapp_images), - formatNumber(stats.image_count, server.serverPreferences.abbreviateCounters), + formatNumber(stats.image_count, serverPreferences.abbreviateCounters), ) TitleValueText( stringResource(R.string.stashapp_stats_image_size), @@ -507,7 +517,7 @@ fun ServerStatsRow( ) TitleValueText( stringResource(R.string.stashapp_stats_total_play_count), - formatNumber(stats.total_play_count, server.serverPreferences.abbreviateCounters), + formatNumber(stats.total_play_count, serverPreferences.abbreviateCounters), ) TitleValueText( stringResource(R.string.stashapp_stats_total_play_duration), @@ -518,7 +528,7 @@ fun ServerStatsRow( ) TitleValueText( stringResource(R.string.stashapp_stats_total_o_count), - formatNumber(stats.total_o_count, server.serverPreferences.abbreviateCounters), + formatNumber(stats.total_o_count, serverPreferences.abbreviateCounters), ) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt index 31009531..2cf0ba97 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt @@ -16,6 +16,7 @@ import androidx.compose.material.icons.filled.Place import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -32,9 +33,6 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.MutableCreationExtras -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -67,6 +65,8 @@ import com.github.damontecres.stashapp.data.OCounter import com.github.damontecres.stashapp.data.SortAndDirection import com.github.damontecres.stashapp.data.SortOption import com.github.damontecres.stashapp.data.StashFindFilter +import com.github.damontecres.stashapp.di.server.MutationEngine +import com.github.damontecres.stashapp.di.server.ServerPreferences import com.github.damontecres.stashapp.filter.extractTitle import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.navigation.NavigationManager @@ -93,18 +93,17 @@ import com.github.damontecres.stashapp.ui.components.scene.SceneDetailsViewModel import com.github.damontecres.stashapp.ui.components.scene.SceneLoadingState import com.github.damontecres.stashapp.ui.components.scene.sceneDetailsBody import com.github.damontecres.stashapp.ui.tryRequestFocus -import com.github.damontecres.stashapp.util.MutationEngine -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.asString import com.github.damontecres.stashapp.util.fakeMarker import com.github.damontecres.stashapp.util.resume_position import com.github.damontecres.stashapp.util.titleOrFilename import com.github.damontecres.stashapp.util.toSeconds import com.github.damontecres.stashapp.views.durationToString +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf @Composable fun SceneDetailsPage( - server: StashServer, navigationManager: NavigationManager, sceneId: String, itemOnClick: ItemOnClicker, @@ -112,20 +111,11 @@ fun SceneDetailsPage( uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, + viewModel: SceneDetailsViewModel = + koinViewModel(key = sceneId) { + parametersOf(sceneId) + }, ) { - val viewModel = - ViewModelProvider.create( - LocalViewModelStoreOwner.current!!, - SceneDetailsViewModel.Factory, - MutableCreationExtras().apply { - set(SceneDetailsViewModel.SERVER_KEY, server) - set(SceneDetailsViewModel.SCENE_ID_KEY, sceneId) - set( - SceneDetailsViewModel.PAGE_SIZE_KEY, - uiConfig.preferences.searchPreferences.maxResults, - ) - }, - )[SceneDetailsViewModel::class] val loadingState by viewModel.loadingState.observeAsState() val tags by viewModel.tags.observeAsState(listOf()) val performers by viewModel.performers.observeAsState(listOf()) @@ -161,8 +151,9 @@ fun SceneDetailsPage( onUpdateTitle?.invoke(AnnotatedString(it)) } } + val server by viewModel.currentServer.collectAsState() SceneDetails( - server = server, + serverPreferences = server.serverPreferences, scene = state.scene, rating100 = rating100, oCount = oCount, @@ -238,7 +229,7 @@ data class DialogParams( @Composable fun SceneDetails( - server: StashServer, + serverPreferences: ServerPreferences, scene: FullSceneData, rating100: Int, oCount: Int, @@ -328,7 +319,7 @@ fun SceneDetails( DefaultLongClicker( navigationManager, itemOnClick, - server.serverPreferences.alwaysStartFromBeginning, + serverPreferences.alwaysStartFromBeginning, markerPlayAllOnClick = { }, ) { showDialog = it } } @@ -445,7 +436,7 @@ fun SceneDetails( onRatingChange = onRatingChange, removeLongClicker = removeLongClicker, studio = studio, - alwaysStartFromBeginning = server.serverPreferences.alwaysStartFromBeginning, + alwaysStartFromBeginning = serverPreferences.alwaysStartFromBeginning, showEditButton = uiConfig.readOnlyModeDisabled, editOnClick = { showDialog = @@ -528,7 +519,7 @@ fun SceneDetails( fromLongClick = false, items = moreOptionsItems( - server = server, + serverPreferences = serverPreferences, context = context, navigationManager = navigationManager, scene = scene, @@ -650,7 +641,7 @@ fun SceneDetails( } fun moreOptionsItems( - server: StashServer, + serverPreferences: ServerPreferences, context: Context, navigationManager: NavigationManager, scene: FullSceneData, @@ -674,7 +665,7 @@ fun moreOptionsItems( Icons.Default.PlayArrow, ) { playOnClick( - if (server.serverPreferences.alwaysStartFromBeginning) { + if (serverPreferences.alwaysStartFromBeginning) { 0L } else { scene.resume_position ?: 0 @@ -691,7 +682,7 @@ fun moreOptionsItems( // TODO show options for other resolutions val format = streamChoice.asString playOnClick( - if (server.serverPreferences.alwaysStartFromBeginning) { + if (serverPreferences.alwaysStartFromBeginning) { 0L } else { scene.resume_position ?: 0 diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/util/ScreenSize.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/util/ScreenSize.kt index f517f5b0..16e7ecec 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/util/ScreenSize.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/util/ScreenSize.kt @@ -1,10 +1,7 @@ package com.github.damontecres.stashapp.ui.util -import android.content.res.Configuration import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalConfiguration @@ -14,21 +11,23 @@ fun screenSize(): ScreenSize { val orientation = LocalConfiguration.current.orientation val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass - return if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - when (windowSizeClass.heightSizeClass) { - WindowHeightSizeClass.Compact -> ScreenSize.COMPACT - WindowHeightSizeClass.Medium -> ScreenSize.MEDIUM - WindowHeightSizeClass.Expanded -> ScreenSize.EXPANDED - else -> ScreenSize.MEDIUM - } - } else { - when (windowSizeClass.widthSizeClass) { - WindowWidthSizeClass.Compact -> ScreenSize.COMPACT - WindowWidthSizeClass.Medium -> ScreenSize.MEDIUM - WindowWidthSizeClass.Expanded -> ScreenSize.EXPANDED - else -> ScreenSize.MEDIUM - } - } +// return if (orientation == Configuration.ORIENTATION_LANDSCAPE) { +// when (windowSizeClass.heightSizeClass) { +// WindowHeightSizeClass.Compact -> ScreenSize.COMPACT +// WindowHeightSizeClass.Medium -> ScreenSize.MEDIUM +// WindowHeightSizeClass.Expanded -> ScreenSize.EXPANDED +// else -> ScreenSize.MEDIUM +// } +// } else { +// when (windowSizeClass.widthSizeClass) { +// WindowWidthSizeClass.Compact -> ScreenSize.COMPACT +// WindowWidthSizeClass.Medium -> ScreenSize.MEDIUM +// WindowWidthSizeClass.Expanded -> ScreenSize.EXPANDED +// else -> ScreenSize.MEDIUM +// } +// } + // TODO + return ScreenSize.MEDIUM } enum class ScreenSize { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3c18081..c9c7d8c5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ leanback-grid = "1.0.0" lifecycle-process = "2.10.0" lifecycle-viewmodel-compose = "2.10.0" material = "1.13.0" -material3-adaptive = "1.0.0-alpha06" +material3-adaptive = "1.2.0" material3-android = "1.4.0" mockito-core = "5.23.0" mockito-kotlin = "6.3.0" @@ -57,6 +57,11 @@ timber = "5.0.1" koin-bom = "4.2.1" koin-plugin = "1.0.0-RC2" wholphin-extensions = "0.2.0" +navigation3 = "1.1.1" +lifecycleViewmodelNav3 = "2.10.0" +material3AdaptiveNav3 = "1.0.0-alpha03" +appcompat = "1.7.1" +activityKtx = "1.13.0" [libraries] acra-dialog = { module = "ch.acra:acra-dialog", version.ref = "acra" } @@ -68,7 +73,7 @@ androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "core-ktx androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle-process" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-process" } -androidx-material3-adaptive = { module = "androidx.compose.material3:material3-adaptive", version.ref = "material3-adaptive" } +androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive-android", version.ref = "material3-adaptive" } androidx-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3-android" } androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" } androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-test-ext-junit" } @@ -158,6 +163,12 @@ koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" } wholphin-extensions-mpv = { module = "com.github.damontecres.wholphin.mpv:wholphin-mpv", version.ref = "wholphin-extensions" } wholphin-extensions-ffmpeg = { module = "com.github.damontecres.wholphin.media3:decoder-ffmpeg", version.ref = "wholphin-extensions" } wholphin-extensions-av1 = { module = "com.github.damontecres.wholphin.media3:decoder-av1", version.ref = "wholphin-extensions" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "navigation3" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" } +androidx-material3-adaptive-navigation3 = { module = "androidx.compose.material3.adaptive:adaptive-navigation3", version.ref = "material3AdaptiveNav3" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "android-application" } From a8e34868c72798f2bac2e31c02bdd88e0abfb485 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 16:09:23 -0400 Subject: [PATCH 03/25] Checkpoint --- .../damontecres/stashapp/MainActivity.kt | 96 ++++- .../stashapp/di/services/ItemClicker.kt | 60 ++++ .../stashapp/ui/ComposeUiConfig.kt | 20 +- .../stashapp/ui/NavDrawerFragment.kt | 3 +- .../stashapp/ui/components/LongClicker.kt | 2 +- .../components/scene/SceneDetailsViewModel.kt | 4 + .../stashapp/ui/nav/ApplicationContent.kt | 327 +++++++++--------- .../damontecres/stashapp/ui/nav/CoilConfig.kt | 12 +- .../stashapp/ui/nav/DestinationContent.kt | 19 +- .../damontecres/stashapp/ui/nav/NavDrawer.kt | 14 +- .../stashapp/ui/nav/NavScaffold.kt | 12 +- .../damontecres/stashapp/ui/pages/MainPage.kt | 5 +- .../stashapp/ui/pages/SceneDetailsPage.kt | 23 +- 13 files changed, 369 insertions(+), 228 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/services/ItemClicker.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt index 719b00cc..9f7009de 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt @@ -1,22 +1,116 @@ package com.github.damontecres.stashapp import android.os.Bundle +import android.util.Log +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.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.lifecycle.lifecycleScope +import androidx.tv.material3.MaterialTheme +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.ui.AppTheme +import com.github.damontecres.stashapp.ui.NavDrawerFragment.Companion.TAG +import com.github.damontecres.stashapp.ui.chooseColorScheme +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.readThemeJson +import com.github.damontecres.stashapp.util.launchDefault import com.github.damontecres.stashapp.util.preferences +import okhttp3.OkHttpClient +import org.koin.android.ext.android.get import org.koin.android.ext.android.inject class MainActivity : AppCompatActivity() { private val navigationManager: NavigationManager by inject() + private val serverRepository: ServerRepository by inject() + + @AuthHttpClient + private val httpClient: OkHttpClient = get() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycleScope.launchDefault { + if (serverRepository.restore()) { + // Success + } else { + // TODO + navigationManager.navigate(Destination.Setup) + } + } setContent { val preferences by this@MainActivity.preferences.data.collectAsState(null) - preferences?.let { + preferences?.let { preferences -> + 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) { + val currentServer by serverRepository.currentServer.collectAsState() + if (currentServer != CurrentServer.UNSET) { + key(currentServer) { + ApplicationContent( + currentServer = currentServer, + preferences = preferences, + navigationManager = navigationManager, + onSwitchServer = { + TODO() + }, + onChangeTheme = { name -> + try { + colorScheme = + chooseColorScheme( + preferences.interfacePreferences.themeStyle, + isSystemInDarkTheme, + if (name.isNullOrBlank() || name == "default") { + defaultColorSchemeSet + } else { + readThemeJson( + this@MainActivity, + name, + ) + }, + ) + Log.i(TAG, "Updated theme") + } catch (ex: Exception) { + Log.e(TAG, "Exception changing theme", ex) + 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 + ) + } + } + } } } } 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 00000000..4a6bca43 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/ItemClicker.kt @@ -0,0 +1,60 @@ +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 org.koin.core.annotation.Single + +@Single +class ItemClicker( + private val context: Application, + private val navigationManager: NavigationManager, +) { + 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/ui/ComposeUiConfig.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/ComposeUiConfig.kt index 1d30a247..38531a34 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/ComposeUiConfig.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/ComposeUiConfig.kt @@ -1,9 +1,8 @@ package com.github.damontecres.stashapp.ui +import com.github.damontecres.stashapp.di.server.ServerPreferences import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.ui.components.StarRatingPrecision -import com.github.damontecres.stashapp.util.ServerPreferences -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.views.models.CardUiSettings import com.github.damontecres.stashapp.views.models.ServerViewModel.Companion.cardSettings @@ -27,27 +26,22 @@ data class ComposeUiConfig( companion object { fun fromStashServer( preferences: StashPreferences, - server: StashServer, + serverPreferences: ServerPreferences, ): ComposeUiConfig = ComposeUiConfig( preferences = preferences, - ratingAsStars = server.serverPreferences.ratingsAsStars, + ratingAsStars = serverPreferences.ratingsAsStars, starPrecision = - StarRatingPrecision.fromFloat( - server.serverPreferences.preferences.getFloat( - ServerPreferences.PREF_RATING_PRECISION, - 1.0f, - ), - ), - showStudioAsText = server.serverPreferences.showStudioAsText, + StarRatingPrecision.fromFloat(serverPreferences.starPrecision), + showStudioAsText = serverPreferences.showStudioAsText, debugTextEnabled = preferences.playbackPreferences.showDebugInfo, cardSettings = preferences.cardSettings, showTitleDuringPlayback = true, - showCardProgress = !server.serverPreferences.alwaysStartFromBeginning, + showCardProgress = !serverPreferences.alwaysStartFromBeginning, playSoundOnFocus = preferences.interfacePreferences.playMovementSounds, readOnlyModeEnabled = preferences.pinPreferences.readOnlyPin.isNotNullOrBlank(), persistVideoFilters = preferences.playbackPreferences.saveVideoFilters, - sfwMode = server.serverPreferences.sfwMode, + sfwMode = serverPreferences.sfwMode, ) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt index edbd056a..79118b61 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt @@ -29,7 +29,6 @@ import com.github.damontecres.stashapp.navigation.NavigationManagerCompose import com.github.damontecres.stashapp.ui.components.server.InitialSetup import com.github.damontecres.stashapp.ui.components.server.ManageServers import com.github.damontecres.stashapp.ui.nav.ApplicationContent -import com.github.damontecres.stashapp.ui.nav.CoilConfig import com.github.damontecres.stashapp.util.preferences import com.github.damontecres.stashapp.views.models.ServerViewModel import dev.olshevski.navigation.reimagined.NavBackHandler @@ -62,7 +61,7 @@ class NavDrawerFragment : Fragment(R.layout.compose_frame) { val preferences by context.preferences.data.collectAsState(null) val isSystemInDarkTheme = isSystemInDarkTheme() preferences?.let { preferences -> - CoilConfig(serverViewModel, preferences) +// CoilConfig(serverViewModel, preferences) var colorScheme by remember { mutableStateOf( diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/LongClicker.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/LongClicker.kt index 91d456ce..0f98d9fc 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/LongClicker.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/LongClicker.kt @@ -12,10 +12,10 @@ import com.github.damontecres.stashapp.api.fragment.MarkerData import com.github.damontecres.stashapp.api.fragment.SlimSceneData import com.github.damontecres.stashapp.api.fragment.StashData import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.filter.extractTitle import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.navigation.FilterAndPosition -import com.github.damontecres.stashapp.navigation.NavigationManager import com.github.damontecres.stashapp.playback.PlaybackMode import com.github.damontecres.stashapp.ui.pages.DialogParams import com.github.damontecres.stashapp.ui.pages.MAX_PLAYLIST_SIZE diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt index 80051113..614fc7cb 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/scene/SceneDetailsViewModel.kt @@ -20,6 +20,8 @@ import com.github.damontecres.stashapp.data.OCounter 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.ItemClicker +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.suppliers.DataSupplierFactory @@ -51,6 +53,8 @@ class SceneDetailsViewModel( private val mutationEngine: MutationEngine, private val serverRepository: ServerRepository, private val preferences: DataStore, + val navigationManager: NavigationManager, + val itemClicker: ItemClicker, @InjectedParam val sceneId: String, ) : ViewModel() { private val exceptionHandler = diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt index 4a6f50b9..d77435f5 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt @@ -13,13 +13,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.window.DialogProperties +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.ui.NavDisplay import com.github.damontecres.stashapp.PreferenceScreenOption import com.github.damontecres.stashapp.api.fragment.ImageData import com.github.damontecres.stashapp.api.fragment.StashData import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.di.server.CurrentServer +import com.github.damontecres.stashapp.di.server.StashServer +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.navigation.FilterAndPosition -import com.github.damontecres.stashapp.navigation.NavigationManagerCompose import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig @@ -30,9 +34,6 @@ import com.github.damontecres.stashapp.ui.components.DialogPopup import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.MarkerDurationDialog import com.github.damontecres.stashapp.ui.pages.DialogParams -import com.github.damontecres.stashapp.util.StashServer -import dev.olshevski.navigation.reimagined.NavController -import dev.olshevski.navigation.reimagined.NavHost /** * Shows the actual compose content of the application @@ -41,20 +42,19 @@ import dev.olshevski.navigation.reimagined.NavHost */ @Composable fun ApplicationContent( - server: StashServer, + currentServer: CurrentServer, preferences: StashPreferences, - navigationManager: NavigationManagerCompose, - navController: NavController, + navigationManager: NavigationManager, onChangeTheme: (String?) -> Unit, onSwitchServer: (StashServer) -> Unit, modifier: Modifier = Modifier, ) { val context = LocalContext.current - var composeUiConfig by remember(server) { + var composeUiConfig by remember(currentServer, preferences) { mutableStateOf( ComposeUiConfig.fromStashServer( preferences, - server, + currentServer.serverPreferences, ), ) } @@ -109,7 +109,7 @@ fun ApplicationContent( DefaultLongClicker( navigationManager, itemOnClick, - server.serverPreferences.alwaysStartFromBeginning, + currentServer.serverPreferences.alwaysStartFromBeginning, markerPlayAllOnClick = { showMarkerDialog = it }, ) { dialogParams = it } } @@ -120,7 +120,7 @@ fun ApplicationContent( add(DrawerPage.HomePage) addAll( DataType.entries - .filter { server.serverPreferences.showMenuItem(it) } + .filter { it in currentServer.serverPreferences.menuItems } .map { DrawerPage.DataTypePage(it) }, ) add(DrawerPage.SettingPage) @@ -134,176 +134,173 @@ fun ApplicationContent( // transitionSpec = DestinationTransitionSpec(), // modifier = modifier, // ) { destination -> - NavHost( - controller = navController, + NavDisplay( + backStack = navigationManager.backStack, + onBack = { navigationManager.goBack() }, modifier = modifier, - ) { destination -> - LaunchedEffect(Unit) { - // Refresh server preferences on each page change - navigationManager.serverViewModel.updateServerPreferences() - composeUiConfig = ComposeUiConfig.fromStashServer(preferences, server) + entryProvider = { destination -> + NavEntry(destination, contentKey = destination.toString()) { + LaunchedEffect(Unit) { + // TODO Refresh server preferences on each page change + } + val fullScreen = + if (isTvDevice) destination.fullScreen else destination.fullScreenTouch - navigationManager.previousDestination = destination - navigationManager.serverViewModel.setCurrentDestination(destination) - } - val fullScreen = if (isTvDevice) destination.fullScreen else destination.fullScreenTouch + if (fullScreen) { + DestinationContent( + currentServer = currentServer, + destination = destination, + composeUiConfig = composeUiConfig, + itemOnClick = itemOnClick, + longClicker = longClicker, + onChangeTheme = onChangeTheme, + onSwitchServer = onSwitchServer, + modifier = Modifier.fillMaxSize(), + onUpdateTitle = null, + ) + } else { + // Highlight on the nav drawer as user navigates around the app + selectedScreen = + when (destination) { + Destination.Main -> { + DrawerPage.HomePage + } - if (fullScreen) { - DestinationContent( - navManager = navigationManager, - server = server, - destination = destination, - composeUiConfig = composeUiConfig, - itemOnClick = itemOnClick, - longClicker = longClicker, - onChangeTheme = onChangeTheme, - onSwitchServer = onSwitchServer, - modifier = Modifier.fillMaxSize(), - onUpdateTitle = null, - ) - } else { - // Highlight on the nav drawer as user navigates around the app - selectedScreen = - when (destination) { - Destination.Main -> { - DrawerPage.HomePage - } + Destination.Search -> { + DrawerPage.SearchPage + } - Destination.Search -> { - DrawerPage.SearchPage - } + Destination.SettingsPin, + is Destination.Settings, + -> { + DrawerPage.SettingPage + } - Destination.SettingsPin, - is Destination.Settings, - -> { - DrawerPage.SettingPage - } + is Destination.Item -> { + pages.firstOrNull { + it is DrawerPage.DataTypePage && it.dataType == destination.dataType + } + } - is Destination.Item -> { - pages.firstOrNull { - it is DrawerPage.DataTypePage && it.dataType == destination.dataType - } - } + is Destination.MarkerDetails -> { + pages.firstOrNull { + it is DrawerPage.DataTypePage && it.dataType == DataType.MARKER + } + } - is Destination.MarkerDetails -> { - pages.firstOrNull { - it is DrawerPage.DataTypePage && it.dataType == DataType.MARKER - } - } + is Destination.Filter -> { + pages.firstOrNull { + it is DrawerPage.DataTypePage && it.dataType == destination.filterArgs.dataType + } + } - is Destination.Filter -> { - pages.firstOrNull { - it is DrawerPage.DataTypePage && it.dataType == destination.filterArgs.dataType + else -> { + null + } } - } - else -> { - null - } - } + val onSelectScreen = { page: DrawerPage -> + val refreshMain = + selectedScreen == DrawerPage.HomePage && page == DrawerPage.HomePage + Log.v( + TAG, + "Navigating to $page", + ) + selectedScreen = page + if (refreshMain) { + navigationManager.goToMain() + } else { + val pageDest = + when (page) { + DrawerPage.HomePage -> { + Destination.Main + } - val onSelectScreen = { page: DrawerPage -> - val refreshMain = - selectedScreen == DrawerPage.HomePage && page == DrawerPage.HomePage - Log.v( - TAG, - "Navigating to $page", - ) - selectedScreen = page - if (refreshMain) { - navigationManager.goToMain() - } else { - val pageDest = - when (page) { - DrawerPage.HomePage -> { - Destination.Main - } + DrawerPage.SearchPage -> { + Destination.Search + } - DrawerPage.SearchPage -> { - Destination.Search - } + DrawerPage.SettingPage -> { + if (composeUiConfig.readOnlyModeDisabled) { + Destination.Settings( + PreferenceScreenOption.BASIC, + ) + } else { + Destination.SettingsPin + } + } - DrawerPage.SettingPage -> { - if (composeUiConfig.readOnlyModeDisabled) { - Destination.Settings( - PreferenceScreenOption.BASIC, - ) - } else { - Destination.SettingsPin + is DrawerPage.DataTypePage -> { + Destination.Filter( + currentServer.serverPreferences.defaultFilters[page.dataType]!!, + ) + } } - } + navigationManager.navigateToFromDrawer(pageDest) + } + } - is DrawerPage.DataTypePage -> { - Destination.Filter( - server.serverPreferences.getDefaultFilter( - page.dataType, - ), - ) + if (isTvDevice) { + NavDrawer( + currentServer = currentServer, + navigationManager = navigationManager, + composeUiConfig = composeUiConfig, + destination = destination, + selectedScreen = selectedScreen, + pages = pages, + itemOnClick = itemOnClick, + longClicker = longClicker, + onSelectScreen = onSelectScreen, + onChangeTheme = onChangeTheme, + onSwitchServer = onSwitchServer, + modifier = Modifier, + ) + } else { + NavScaffold( + currentServer = currentServer, + navigationManager = navigationManager, + composeUiConfig = composeUiConfig, + destination = destination, + selectedScreen = selectedScreen, + pages = pages, + itemOnClick = itemOnClick, + longClicker = longClicker, + onSelectScreen = onSelectScreen, + onChangeTheme = onChangeTheme, + onSwitchServer = onSwitchServer, + modifier = Modifier, + ) + } + } + dialogParams?.let { params -> + DialogPopup( + showDialog = true, + title = params.title, + dialogItems = params.items, + onDismissRequest = { dialogParams = null }, + dismissOnClick = true, + waitToLoad = true, + properties = DialogProperties(), + ) + } + if (showMarkerDialog != null) { + MarkerDurationDialog( + onDismissRequest = { showMarkerDialog = null }, + onClick = { + showMarkerDialog?.let { filterAndPosition -> + val dest = + Destination.Playlist( + filterAndPosition.filter, + filterAndPosition.position, + it, + ) + navigationManager.navigate(dest) } - } - navigationManager.navigateFromNavDrawer(pageDest) + showMarkerDialog = null + }, + ) } } - - if (isTvDevice) { - NavDrawer( - server = server, - navigationManager = navigationManager, - composeUiConfig = composeUiConfig, - destination = destination, - selectedScreen = selectedScreen, - pages = pages, - itemOnClick = itemOnClick, - longClicker = longClicker, - onSelectScreen = onSelectScreen, - onChangeTheme = onChangeTheme, - onSwitchServer = onSwitchServer, - modifier = Modifier, - ) - } else { - NavScaffold( - server = server, - navigationManager = navigationManager, - composeUiConfig = composeUiConfig, - destination = destination, - selectedScreen = selectedScreen, - pages = pages, - itemOnClick = itemOnClick, - longClicker = longClicker, - onSelectScreen = onSelectScreen, - onChangeTheme = onChangeTheme, - onSwitchServer = onSwitchServer, - modifier = Modifier, - ) - } - } - dialogParams?.let { params -> - DialogPopup( - showDialog = true, - title = params.title, - dialogItems = params.items, - onDismissRequest = { dialogParams = null }, - dismissOnClick = true, - waitToLoad = true, - properties = DialogProperties(), - ) - } - if (showMarkerDialog != null) { - MarkerDurationDialog( - onDismissRequest = { showMarkerDialog = null }, - onClick = { - showMarkerDialog?.let { filterAndPosition -> - val dest = - Destination.Playlist( - filterAndPosition.filter, - filterAndPosition.position, - it, - ) - navigationManager.navigate(dest) - } - showMarkerDialog = null - }, - ) - } - } + }, + ) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/CoilConfig.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/CoilConfig.kt index 03227edc..2c9d4955 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/CoilConfig.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/CoilConfig.kt @@ -1,6 +1,8 @@ package com.github.damontecres.stashapp.ui.nav import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import coil3.ImageLoader import coil3.annotation.ExperimentalCoilApi import coil3.compose.setSingletonImageLoaderFactory @@ -11,16 +13,17 @@ import coil3.network.okhttp.OkHttpNetworkFetcherFactory import coil3.request.crossfade import coil3.util.DebugLogger import com.github.damontecres.stashapp.proto.StashPreferences -import com.github.damontecres.stashapp.views.models.ServerViewModel import okhttp3.Call +import okhttp3.OkHttpClient import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class, ExperimentalCoilApi::class) @Composable fun CoilConfig( - serverViewModel: ServerViewModel, + httpClient: OkHttpClient, preferences: StashPreferences, ) { + val currentHttpClient by rememberUpdatedState(httpClient) setSingletonImageLoaderFactory { ctx -> val cacheLogging = preferences.cachePreferences.logCacheHits val diskCacheSize = @@ -42,10 +45,7 @@ fun CoilConfig( cacheStrategy = { CacheControlCacheStrategy() }, callFactory = { Call.Factory { request -> - // TODO this seems hacky? - serverViewModel.requireServer().okHttpClient.newCall( - request, - ) + currentHttpClient.newCall(request) } }, ), diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt index ec346ce9..79f493ea 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt @@ -5,8 +5,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.di.server.CurrentServer +import com.github.damontecres.stashapp.di.server.StashServer import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManagerCompose import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.LongClicker @@ -30,7 +31,6 @@ import com.github.damontecres.stashapp.ui.pages.SettingsPage import com.github.damontecres.stashapp.ui.pages.StudioPage import com.github.damontecres.stashapp.ui.pages.TagPage import com.github.damontecres.stashapp.ui.pages.UpdateAppPage -import com.github.damontecres.stashapp.util.StashServer import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -39,8 +39,7 @@ import kotlin.time.Duration.Companion.seconds */ @Composable fun DestinationContent( - navManager: NavigationManagerCompose, - server: StashServer, + currentServer: CurrentServer, destination: Destination, composeUiConfig: ComposeUiConfig, itemOnClick: ItemOnClicker, @@ -194,7 +193,6 @@ fun DestinationContent( Destination.Main -> { MainPage( uiConfig = composeUiConfig, - itemOnClick = itemOnClick, longClicker = longClicker, modifier = modifier, ) @@ -240,19 +238,8 @@ fun DestinationContent( DataType.SCENE -> { SceneDetailsPage( modifier = modifier, - navigationManager = navManager, sceneId = destination.id, - itemOnClick = itemOnClick, uiConfig = composeUiConfig, - playOnClick = { position, mode -> - navManager.navigate( - Destination.Playback( - destination.id, - position, - mode, - ), - ) - }, onUpdateTitle = onUpdateTitle, ) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt index a45511a7..4d5332c4 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt @@ -35,8 +35,10 @@ import androidx.tv.material3.NavigationDrawerItem import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState import com.github.damontecres.stashapp.R +import com.github.damontecres.stashapp.di.server.CurrentServer +import com.github.damontecres.stashapp.di.server.StashServer +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManagerCompose import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.LocalPlayerContext import com.github.damontecres.stashapp.ui.PlayerContext @@ -49,13 +51,12 @@ import com.github.damontecres.stashapp.ui.util.ifElse import com.github.damontecres.stashapp.ui.util.playOnClickSound import com.github.damontecres.stashapp.ui.util.playSoundOnFocus import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.StashServer import kotlinx.coroutines.launch @Composable fun NavDrawer( - server: StashServer, - navigationManager: NavigationManagerCompose, + currentServer: CurrentServer, + navigationManager: NavigationManager, composeUiConfig: ComposeUiConfig, destination: Destination, selectedScreen: DrawerPage?, @@ -168,7 +169,7 @@ fun NavDrawer( isNotTvDevice, Modifier.clickable(onClick = onClick), ), - text = server.url, + text = currentServer.server.url, maxLines = 1, ) } @@ -203,8 +204,7 @@ fun NavDrawer( LocalPlayerContext provides PlayerContext, ) { DestinationContent( - navManager = navigationManager, - server = server, + currentServer = currentServer, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt index 99b3384e..d66d2b92 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt @@ -34,21 +34,22 @@ import androidx.compose.ui.unit.sp import androidx.tv.material3.MaterialTheme import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.di.server.CurrentServer +import com.github.damontecres.stashapp.di.server.StashServer +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManagerCompose import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.FontAwesome import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.LongClicker import com.github.damontecres.stashapp.ui.util.ScreenSize import com.github.damontecres.stashapp.ui.util.screenSize -import com.github.damontecres.stashapp.util.StashServer @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) @Composable fun NavScaffold( - server: StashServer, - navigationManager: NavigationManagerCompose, + currentServer: CurrentServer, + navigationManager: NavigationManager, composeUiConfig: ComposeUiConfig, destination: Destination, selectedScreen: DrawerPage?, @@ -181,8 +182,7 @@ fun NavScaffold( verticalArrangement = Arrangement.spacedBy(16.dp), ) { DestinationContent( - navManager = navigationManager, - server = server, + currentServer = currentServer, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt index 1f0eb5b4..9ceb9467 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MainPage.kt @@ -69,6 +69,7 @@ import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.di.server.QueryEngine import com.github.damontecres.stashapp.di.server.ServerPreferences import com.github.damontecres.stashapp.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.ItemClicker import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.navigation.FilterAndPosition import com.github.damontecres.stashapp.proto.StashPreferences @@ -112,6 +113,7 @@ class MainPageViewModel( private val queryEngine: QueryEngine, private val serverRepository: ServerRepository, private val preferences: DataStore, + val itemClicker: ItemClicker, ) : ViewModel() { val frontPageRows = mutableStateListOf() @@ -189,7 +191,6 @@ class MainPageViewModel( @Composable fun MainPage( uiConfig: ComposeUiConfig, - itemOnClick: ItemOnClicker, longClicker: LongClicker, modifier: Modifier = Modifier, viewModel: MainPageViewModel = koinViewModel(), @@ -224,7 +225,7 @@ fun MainPage( serverStats = serverStats, uiConfig = uiConfig, rows = frontPageRows, - itemOnClick = itemOnClick, + itemOnClick = viewModel.itemClicker::onClick, longClicker = longClicker, ) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt index 2cf0ba97..f62e2f35 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SceneDetailsPage.kt @@ -67,15 +67,14 @@ import com.github.damontecres.stashapp.data.SortOption import com.github.damontecres.stashapp.data.StashFindFilter import com.github.damontecres.stashapp.di.server.MutationEngine import com.github.damontecres.stashapp.di.server.ServerPreferences +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.filter.extractTitle import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManager import com.github.damontecres.stashapp.playback.PlaybackMode import com.github.damontecres.stashapp.proto.StreamChoice import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.FontAwesome -import com.github.damontecres.stashapp.ui.LocalGlobalContext import com.github.damontecres.stashapp.ui.components.CircularProgress import com.github.damontecres.stashapp.ui.components.DefaultLongClicker import com.github.damontecres.stashapp.ui.components.DeleteDialog @@ -104,10 +103,7 @@ import org.koin.core.parameter.parametersOf @Composable fun SceneDetailsPage( - navigationManager: NavigationManager, sceneId: String, - itemOnClick: ItemOnClicker, - playOnClick: (position: Long, mode: PlaybackMode) -> Unit, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, @@ -154,6 +150,7 @@ fun SceneDetailsPage( val server by viewModel.currentServer.collectAsState() SceneDetails( serverPreferences = server.serverPreferences, + navigationManager = viewModel.navigationManager, scene = state.scene, rating100 = rating100, oCount = oCount, @@ -165,8 +162,16 @@ fun SceneDetailsPage( studio = studio, suggestions = suggestions, uiConfig = uiConfig, - itemOnClick = itemOnClick, - playOnClick = playOnClick, + itemOnClick = viewModel.itemClicker::onClick, + playOnClick = { position, mode -> + viewModel.navigationManager.navigate( + Destination.Playback( + sceneId, + position, + mode, + ), + ) + }, addItem = { item -> when (item) { is PerformerData, is SlimPerformerData -> viewModel.addPerformer(item.id) @@ -200,7 +205,7 @@ fun SceneDetailsPage( viewModel.deleteScene(deleteFiles, deleteGenerated) { if (it) { Toast.makeText(context, "Deleted", Toast.LENGTH_SHORT).show() - navigationManager.goBack() + viewModel.navigationManager.goBack() } else { Toast .makeText(context, "Error deleting scene", Toast.LENGTH_SHORT) @@ -230,6 +235,7 @@ data class DialogParams( @Composable fun SceneDetails( serverPreferences: ServerPreferences, + navigationManager: com.github.damontecres.stashapp.di.services.NavigationManager, scene: FullSceneData, rating100: Int, oCount: Int, @@ -252,7 +258,6 @@ fun SceneDetails( showRatingBar: Boolean = true, ) { val context = LocalContext.current - val navigationManager = LocalGlobalContext.current.navigationManager var showDialog by remember { mutableStateOf(null) } var showDetailsDialog by remember { mutableStateOf(false) } From 3d7571dfba5abec29265623689d330803ad4e77b Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 17:02:05 -0400 Subject: [PATCH 04/25] Checkpoint 2 --- .../damontecres/stashapp/di/AppModule.kt | 25 +----- .../stashapp/di/server/MutationEngine.kt | 46 ++++++++++ .../stashapp/di/server/ServerInterceptor.kt | 34 ++++++++ .../stashapp/di/server/StashApi.kt | 43 ++++++---- .../playback/TrackActivityPlaybackListener.kt | 26 +----- .../playback/PlaybackPageContent.kt | 48 ++++++----- .../playback/SceneDetailsOverlay.kt | 2 - .../components/prefs/ComposablePreference.kt | 26 +++--- .../ui/components/prefs/PreferencesContent.kt | 28 +++--- .../components/prefs/PreferencesViewModel.kt | 47 +++++++--- .../ui/components/server/ManageServers.kt | 11 +-- .../server/ManageServersViewModel.kt | 86 +++++++++++-------- .../stashapp/ui/nav/ApplicationContent.kt | 1 + .../stashapp/ui/nav/DestinationContent.kt | 6 +- .../damontecres/stashapp/ui/nav/NavDrawer.kt | 1 + .../stashapp/ui/nav/NavScaffold.kt | 1 + .../stashapp/ui/pages/PlaybackPage.kt | 13 +-- .../ui/pages/PlaybackPageViewModel.kt | 52 ++++++----- .../stashapp/ui/pages/SettingsPage.kt | 6 -- .../stashapp/ui/pages/UpdateAppPage.kt | 2 +- 20 files changed, 303 insertions(+), 201 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/server/ServerInterceptor.kt 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 index 743a543a..3e060d46 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/di/AppModule.kt @@ -5,9 +5,9 @@ import android.os.Build import androidx.datastore.core.DataStore import co.touchlab.kermit.Logger import com.github.damontecres.stashapp.R +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.Constants import com.github.damontecres.stashapp.util.joinNotNullOrBlank import com.github.damontecres.stashapp.util.joinValueNotNull import com.github.damontecres.stashapp.util.preferences @@ -55,28 +55,7 @@ fun provideAuthHttpClient( .newBuilder() .addInterceptor { val server = serverRepository.currentServer.value.server - val request = - if (server.apiKey != null) { - val isStashUrl = - it - .request() - .url - .toString() - .startsWith(server.serverRoot) - if (isStashUrl) { - // Only set the API Key if the target URL is the stash server - it - .request() - .newBuilder() - .addHeader(Constants.STASH_API_HEADER, server.cleanedApiKey!!) - .build() - } else { - it.request() - } - } else { - it.request() - } - it.proceed(request) + ServerInterceptor(server).intercept(it) }.build() fun createUserAgent(context: Context): String { diff --git a/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt index 3302b559..cf2d8e6d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/di/server/MutationEngine.kt @@ -15,6 +15,8 @@ import com.github.damontecres.stashapp.api.ImageDecrementOMutation import com.github.damontecres.stashapp.api.ImageIncrementOMutation import com.github.damontecres.stashapp.api.ImageResetOMutation import com.github.damontecres.stashapp.api.InstallPackagesMutation +import com.github.damontecres.stashapp.api.MetadataGenerateMutation +import com.github.damontecres.stashapp.api.MetadataScanMutation import com.github.damontecres.stashapp.api.SaveFilterMutation import com.github.damontecres.stashapp.api.SceneAddOMutation import com.github.damontecres.stashapp.api.SceneAddPlayCountMutation @@ -37,6 +39,7 @@ import com.github.damontecres.stashapp.api.fragment.SavedFilter import com.github.damontecres.stashapp.api.fragment.StudioData import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.api.type.GalleryUpdateInput +import com.github.damontecres.stashapp.api.type.GenerateMetadataInput import com.github.damontecres.stashapp.api.type.GroupCreateInput import com.github.damontecres.stashapp.api.type.GroupUpdateInput import com.github.damontecres.stashapp.api.type.ImageUpdateInput @@ -45,6 +48,7 @@ import com.github.damontecres.stashapp.api.type.PackageType import com.github.damontecres.stashapp.api.type.PerformerCreateInput import com.github.damontecres.stashapp.api.type.PerformerUpdateInput import com.github.damontecres.stashapp.api.type.SaveFilterInput +import com.github.damontecres.stashapp.api.type.ScanMetadataInput import com.github.damontecres.stashapp.api.type.SceneDestroyInput import com.github.damontecres.stashapp.api.type.SceneGroupInput import com.github.damontecres.stashapp.api.type.SceneMarkerCreateInput @@ -66,6 +70,7 @@ import org.koin.core.annotation.Single @Single class MutationEngine( api: StashApi, + private val serverRepository: ServerRepository, ) : StashEngine(api) { var readOnlyMode = false @@ -130,6 +135,47 @@ class MutationEngine( return result.data!!.sceneAddPlay.count } + suspend fun triggerScan(): String { + val config = serverRepository.currentServer.value.serverPreferences.scanConfig + val mutation = + MetadataScanMutation( + ScanMetadataInput( + 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 { + val config = serverRepository.currentServer.value.serverPreferences.generateConfig + val mutation = + MetadataGenerateMutation( + GenerateMetadataInput( + 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 + } + suspend fun setTagsOnScene( sceneId: String, tagIds: List, 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 00000000..bfb04c76 --- /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/StashApi.kt b/app/src/main/java/com/github/damontecres/stashapp/di/server/StashApi.kt index 074da8d4..bdd81180 100644 --- 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 @@ -10,7 +10,7 @@ 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.AuthHttpClient +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 @@ -34,7 +34,7 @@ import kotlin.time.DurationUnit @Single class StashApi( private val context: Context, - @param:AuthHttpClient private val httpClient: OkHttpClient, + @param:StandardHttpClient private val httpClient: OkHttpClient, private val preferences: DataStore, ) { @OptIn(ApolloExperimental::class) @@ -44,10 +44,20 @@ class StashApi( lateinit var apolloClient: ApolloClient private set - @OptIn(ApolloExperimental::class) suspend fun changeServer(server: StashServer) { Timber.i("Switching server to %s", server.url) - val client = createOkHttpClient() + this.apolloClient = createApolloClient(server) + this.server = server + } + + suspend fun createFor(server: StashServer) = + StashApi(context, httpClient, preferences).apply { + changeServer(server) + } + + @OptIn(ApolloExperimental::class) + suspend fun createApolloClient(server: StashServer): ApolloClient { + val client = createOkHttpClient(server) val apolloClient = ApolloClient .Builder() @@ -61,11 +71,10 @@ class StashApi( .webSocketEngine(WebSocketEngine(client)) .build(), ).build() - this.apolloClient = apolloClient - this.server = server + return apolloClient } - private suspend fun createOkHttpClient(): OkHttpClient { + private suspend fun createOkHttpClient(server: StashServer): OkHttpClient { val preferences = preferences.data.first() val trustAll = preferences.advancedPreferences.trustSelfSignedCertificates val cacheDuration = @@ -138,15 +147,17 @@ class StashApi( CacheControl.Builder().noCache().build() } builder = - builder.addNetworkInterceptor { - val request = - it - .request() - .newBuilder() - .cacheControl(cacheControl) - .build() - it.proceed(request) - } + 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)) diff --git a/app/src/main/java/com/github/damontecres/stashapp/playback/TrackActivityPlaybackListener.kt b/app/src/main/java/com/github/damontecres/stashapp/playback/TrackActivityPlaybackListener.kt index f3b02fb3..58040f1b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/playback/TrackActivityPlaybackListener.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/playback/TrackActivityPlaybackListener.kt @@ -5,9 +5,8 @@ import androidx.annotation.OptIn import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi import com.github.damontecres.stashapp.data.Scene -import com.github.damontecres.stashapp.util.MutationEngine +import com.github.damontecres.stashapp.di.server.MutationEngine import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.toSeconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -24,15 +23,14 @@ import kotlin.time.Duration.Companion.seconds */ @OptIn(UnstableApi::class) class TrackActivityPlaybackListener( - private val server: StashServer, + private val mutationEngine: MutationEngine, + private val minimumPlayPercent: Float, dispatcher: CoroutineDispatcher = Dispatchers.Main, val scene: Scene, private val getCurrentPosition: () -> Long, ) : Player.Listener { - private val mutationEngine = MutationEngine(server) private val coroutineScope = CoroutineScope(dispatcher) private val task: TimerTask - private val minimumPlayPercent = server.serverPreferences.minimumPlayPercent private val maxPlayPercent: Int = 98 private var totalPlayDurationMilliseconds = AtomicLong(0) @@ -156,24 +154,6 @@ class TrackActivityPlaybackListener( } } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TrackActivityPlaybackListener - - if (server != other.server) return false - if (scene != other.scene) return false - - return true - } - - override fun hashCode(): Int { - var result = server.hashCode() - result = 31 * result + scene.hashCode() - return result - } - companion object { private const val TAG = "TrackActivityPlaybackListener" diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt index f9658a18..15f84983 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt @@ -89,6 +89,11 @@ import com.github.damontecres.stashapp.api.fragment.StashData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.VideoFilter import com.github.damontecres.stashapp.data.room.PlaybackEffect +import com.github.damontecres.stashapp.di.AuthHttpClient +import com.github.damontecres.stashapp.di.server.CurrentServer +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.navigation.NavigationManager import com.github.damontecres.stashapp.playback.PlaylistFragment import com.github.damontecres.stashapp.playback.TrackActivityPlaybackListener @@ -112,12 +117,7 @@ import com.github.damontecres.stashapp.ui.indexOfFirstOrNull import com.github.damontecres.stashapp.ui.pages.SearchForDialog import com.github.damontecres.stashapp.ui.tryRequestFocus import com.github.damontecres.stashapp.util.ComposePager -import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.MutationEngine -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.findActivity import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.launchDefault @@ -138,7 +138,9 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient import okhttp3.Request +import org.koin.core.annotation.KoinViewModel import timber.log.Timber import java.util.Locale import kotlin.properties.Delegates @@ -148,10 +150,14 @@ import kotlin.time.Duration.Companion.milliseconds const val TAG = "PlaybackPageContent" -class PlaybackViewModel : - ViewModel(), +@KoinViewModel +class PlaybackViewModel( + @param:AuthHttpClient private val httpClient: OkHttpClient, + private val serverRepository: ServerRepository, + private val queryEngine: QueryEngine, + private val mutationEngine: MutationEngine, +) : ViewModel(), Player.Listener { - private lateinit var server: StashServer private lateinit var exceptionHandler: CoroutineExceptionHandler private lateinit var uiConfig: ComposeUiConfig private var markersEnabled by Delegates.notNull() @@ -178,7 +184,6 @@ class PlaybackViewModel : .stateIn(viewModelScope, SharingStarted.Lazily, VideoFilter()) fun init( - server: StashServer, player: Player, trackActivity: Boolean, markersEnabled: Boolean, @@ -186,13 +191,12 @@ class PlaybackViewModel : videoFiltersEnabled: Boolean, uiConfig: ComposeUiConfig, ) { - this.server = server this.player = player this.trackActivity = trackActivity this.markersEnabled = markersEnabled this.saveFilters = saveFilters this.videoFiltersEnabled = videoFiltersEnabled - this.exceptionHandler = LoggingCoroutineExceptionHandler(server, viewModelScope) + this.exceptionHandler = CoroutineExceptionHandler { context, throwable -> } player.addListener(this) addCloseable { player.removeListener(this@PlaybackViewModel) } if (trackActivity) { @@ -228,7 +232,8 @@ class PlaybackViewModel : tag.item.let { trackActivityListener = TrackActivityPlaybackListener( - server = server, + mutationEngine = mutationEngine, + minimumPlayPercent = TODO(), scene = it, getCurrentPosition = { player.currentPosition @@ -244,6 +249,7 @@ class PlaybackViewModel : updateVideoFilter(VideoFilter()) if (saveFilters && videoFiltersEnabled) { viewModelScope.launch(sceneJob + StashCoroutineExceptionHandler() + Dispatchers.IO) { + val server = serverRepository.currentServer.value.server val vf = StashApplication .getDatabase() @@ -298,7 +304,7 @@ class PlaybackViewModel : withContext(Dispatchers.Default) { val res = withContext(Dispatchers.IO) { - server.okHttpClient + httpClient .newCall( Request.Builder().url(vttUrl).build(), ).execute() @@ -307,7 +313,8 @@ class PlaybackViewModel : res.body.use { it?.bytes()?.let { try { - val baseUrl = StashClient.getServerRoot(server.url) + val server = serverRepository.currentServer.value.server + val baseUrl = server.serverRoot val regex = Regex("(\\w+\\.\\w+)#xywh=(\\d+),(\\d+),(\\d+),(\\d+)") val spriteData = mutableListOf() WebvttParser().parse(it, SubtitleParser.OutputOptions.allCues()) { @@ -353,7 +360,6 @@ class PlaybackViewModel : private fun refreshScene(sceneId: String) { // Fetch o-count & markers viewModelScope.launch(sceneJob + exceptionHandler) { - val queryEngine = QueryEngine(server) val scene = queryEngine.getScene(sceneId) if (scene != null) { val markers = @@ -393,7 +399,6 @@ class PlaybackViewModel : ) { state.value.mediaItemTag?.let { viewModelScope.launch(exceptionHandler) { - val mutationEngine = MutationEngine(server) val newMarker = mutationEngine.createMarker(it.item.id, position, tagId) if (newMarker != null) { // Refresh markers @@ -411,7 +416,6 @@ class PlaybackViewModel : fun incrementOCount(sceneId: String) { viewModelScope.launchIO(exceptionHandler) { - val mutationEngine = MutationEngine(server) val result = mutationEngine.incrementOCounter(sceneId).count _state.update { it.copy(oCount = result) } } @@ -426,6 +430,7 @@ class PlaybackViewModel : viewModelScope.launchIO(StashCoroutineExceptionHandler(autoToast = true)) { val vf = state.value.videoFilter if (vf != null) { + val server = serverRepository.currentServer.value.server StashApplication .getDatabase() .playbackEffectsDao() @@ -449,8 +454,7 @@ class PlaybackViewModel : rating100: Int, ) { viewModelScope.launch(exceptionHandler) { - val newRating = - MutationEngine(server).setRating(sceneId, rating100)?.rating100 ?: 0 + val newRating = mutationEngine.setRating(sceneId, rating100)?.rating100 ?: 0 _state.update { it.copy(rating100 = newRating) } showSetRatingToast(StashApplication.getApplication(), newRating) } @@ -583,7 +587,7 @@ data class PlaybackInfo( @OptIn(UnstableApi::class) @Composable fun PlaybackPageContent( - server: StashServer, + currentServer: CurrentServer, player: Player, playlist: List, startIndex: Int, @@ -687,10 +691,9 @@ fun PlaybackPageContent( LaunchedEffect(Unit) { viewModel.init( - server, player, uiConfig.preferences.playbackPreferences.savePlayHistory && - server.serverPreferences.trackActivity && + currentServer.serverPreferences.trackActivity && !isMarkerPlaylist, markersEnabled, uiConfig.persistVideoFilters, @@ -1128,7 +1131,6 @@ fun PlaybackPageContent( Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background.copy(alpha = .75f)), - server = server, scene = scene, performers = performers, uiConfig = uiConfig, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/SceneDetailsOverlay.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/SceneDetailsOverlay.kt index 22c77578..97800519 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/SceneDetailsOverlay.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/SceneDetailsOverlay.kt @@ -22,11 +22,9 @@ import com.github.damontecres.stashapp.ui.components.scene.SceneDescriptionDialo import com.github.damontecres.stashapp.ui.components.scene.SceneDetailsFooter import com.github.damontecres.stashapp.ui.components.scene.SceneDetailsHeaderInfo import com.github.damontecres.stashapp.ui.components.scene.sceneDetailsBody -import com.github.damontecres.stashapp.util.StashServer @Composable fun SceneDetailsOverlay( - server: StashServer, scene: FullSceneData, performers: List, rating100: Int, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/ComposablePreference.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/ComposablePreference.kt index e050d5f0..20a16918 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/ComposablePreference.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/ComposablePreference.kt @@ -42,7 +42,8 @@ import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.SettingsFragment -import com.github.damontecres.stashapp.navigation.NavigationManager +import com.github.damontecres.stashapp.di.server.CurrentServer +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.ui.compat.Button import com.github.damontecres.stashapp.ui.components.DialogItem import com.github.damontecres.stashapp.ui.components.DialogPopup @@ -50,25 +51,23 @@ import com.github.damontecres.stashapp.ui.components.EditTextBox import com.github.damontecres.stashapp.ui.components.server.ConfigurePin import com.github.damontecres.stashapp.ui.pages.DialogParams import com.github.damontecres.stashapp.util.AppUpgradeHandler -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.plugin.CompanionPlugin -import com.github.damontecres.stashapp.util.testStashConnection import com.github.damontecres.stashapp.views.formatBytes import kotlinx.coroutines.launch @Suppress("UNCHECKED_CAST") @Composable fun ComposablePreference( - server: StashServer, + currentServer: CurrentServer, navigationManager: NavigationManager, preference: StashPreference, value: T?, onValueChange: (T) -> Unit, cacheUsage: CacheUsage, onCacheClear: () -> Unit, + onTriggerScan: () -> Unit, + onTriggerGenerate: () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { @@ -86,11 +85,13 @@ fun ComposablePreference( scope.launch(StashCoroutineExceptionHandler()) { when (preference) { StashPreference.CurrentServer -> { - testStashConnection(context, true, server.apolloClient) + TODO() +// testStashConnection(context, true, server.apolloClient) } StashPreference.SendLogs -> { - CompanionPlugin.sendLogCat(context, server, false) + TODO() +// CompanionPlugin.sendLogCat(context, server, false) } StashPreference.CacheClear -> { @@ -99,11 +100,11 @@ fun ComposablePreference( } StashPreference.TriggerScan -> { - MutationEngine(server).triggerScan() + onTriggerScan.invoke() } StashPreference.TriggerGenerate -> { - MutationEngine(server).triggerGenerate() + onTriggerGenerate.invoke() } StashPreference.MigratePreferences -> { @@ -122,7 +123,8 @@ fun ComposablePreference( scope.launch(StashCoroutineExceptionHandler()) { when (preference) { StashPreference.SendLogs -> { - CompanionPlugin.sendLogCat(context, server, true) + TODO() +// CompanionPlugin.sendLogCat(context, server, true) } else -> { @@ -137,7 +139,7 @@ fun ComposablePreference( ClickPreference( title = title, onClick = onClick, - summary = server.url, + summary = currentServer.server.url, interactionSource = interactionSource, modifier = modifier, ) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesContent.kt index 11b2332a..0edddb9b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesContent.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf @@ -38,7 +39,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.edit -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.preference.PreferenceManager import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text @@ -46,7 +46,6 @@ import com.github.damontecres.stashapp.PreferenceScreenOption import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.RootActivity import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManager import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.components.screensaver.ChooseScreensaverFilterDialog import com.github.damontecres.stashapp.ui.tryRequestFocus @@ -55,11 +54,11 @@ import com.github.damontecres.stashapp.ui.util.playOnClickSound import com.github.damontecres.stashapp.ui.util.playSoundOnFocus 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.isNotNullOrBlank import com.github.damontecres.stashapp.util.preferences import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel val basicPreferences = listOf( @@ -237,13 +236,11 @@ val advancedPreferences = @Composable fun PreferencesContent( - server: StashServer, - navigationManager: NavigationManager, uiConfig: ComposeUiConfig, preferenceScreenOption: PreferenceScreenOption, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, - viewModel: PreferencesViewModel = viewModel(), + viewModel: PreferencesViewModel = koinViewModel(), ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -257,6 +254,7 @@ fun PreferencesContent( preferences = it } } + val currentServer by viewModel.serverRepository.currentServer.collectAsState() val movementSounds = preferences.interfacePreferences.playMovementSounds val installedVersion = remember { UpdateChecker.getInstalledVersion(context) } @@ -286,7 +284,7 @@ fun PreferencesContent( LaunchedEffect(Unit) { onUpdateTitle?.invoke(AnnotatedString(screenTitle)) if (preferenceScreenOption == PreferenceScreenOption.ADVANCED) { - viewModel.init(context, server) + viewModel.init() } } val jobQueue by viewModel.runningJobs.observeAsState(listOf()) @@ -360,7 +358,7 @@ fun PreferencesContent( onClick = { if (movementSounds) playOnClickSound(context) updateVersion?.let { - navigationManager.navigate(Destination.UpdateApp(it)) + viewModel.navigationManager.navigate(Destination.UpdateApp(it)) } }, summary = updateVersion?.version?.toString(), @@ -391,7 +389,7 @@ fun PreferencesContent( if (movementSounds) playOnClickSound(context) if (clickCount++ >= 2) { clickCount = 0 - navigationManager.navigate(Destination.Debug) + viewModel.navigationManager.navigate(Destination.Debug) } }, summary = installedVersion.toString(), @@ -419,7 +417,7 @@ fun PreferencesContent( if (movementSounds) playOnClickSound(context) if (updateVersion != null && updateAvailable) { updateVersion?.let { - navigationManager.navigate( + viewModel.navigationManager.navigate( Destination.UpdateApp(it), ) } @@ -436,7 +434,7 @@ fun PreferencesContent( onLongClick = { if (movementSounds) playOnClickSound(context) updateVersion?.let { - navigationManager.navigate( + viewModel.navigationManager.navigate( Destination.UpdateApp(it), ) } @@ -476,8 +474,8 @@ fun PreferencesContent( else -> { val value = pref.getter.invoke(preferences) ComposablePreference( - server = server, - navigationManager = navigationManager, + currentServer = currentServer, + navigationManager = viewModel.navigationManager, preference = pref, value = value, onValueChange = { newValue -> @@ -524,9 +522,11 @@ fun PreferencesContent( } } }, - onCacheClear = { viewModel.updateCacheUsage(context) }, + onCacheClear = { viewModel.updateCacheUsage() }, cacheUsage = cacheUsage, interactionSource = interactionSource, + onTriggerScan = viewModel::onTriggerScan, + onTriggerGenerate = viewModel::onTriggerGenerate, modifier = Modifier .ifElse( diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesViewModel.kt index 584259a1..7be990ff 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/prefs/PreferencesViewModel.kt @@ -1,6 +1,6 @@ package com.github.damontecres.stashapp.ui.components.prefs -import android.content.Context +import android.app.Application import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel @@ -9,18 +9,30 @@ import coil3.imageLoader import com.github.damontecres.stashapp.api.JobQueueQuery import com.github.damontecres.stashapp.api.fragment.StashJob import com.github.damontecres.stashapp.api.type.JobStatusUpdateType +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.server.SubscriptionEngine +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.ui.indexOfFirstOrNull import com.github.damontecres.stashapp.util.Constants -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.SubscriptionEngine +import com.github.damontecres.stashapp.util.launchDefault import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.koin.core.annotation.KoinViewModel -class PreferencesViewModel : ViewModel() { +@KoinViewModel +class PreferencesViewModel( + private val context: Application, + private val queryEngine: QueryEngine, + private val mutationEngine: MutationEngine, + private val subscriptionEngine: SubscriptionEngine, + val navigationManager: NavigationManager, + val serverRepository: ServerRepository, +) : ViewModel() { private val lock = Mutex() val runningJobs = MutableLiveData>(listOf()) val cacheUsage = @@ -33,13 +45,9 @@ class PreferencesViewModel : ViewModel() { ), ) - fun init( - context: Context, - server: StashServer, - ) { + fun init() { runningJobs.value = listOf() viewModelScope.launch(StashCoroutineExceptionHandler()) { - val queryEngine = QueryEngine(server) runningJobs.value = queryEngine .executeQuery(JobQueueQuery()) @@ -48,7 +56,6 @@ class PreferencesViewModel : ViewModel() { ?.map { it.stashJob } .orEmpty() - val subscriptionEngine = SubscriptionEngine(server) subscriptionEngine.subscribeToJobs { update -> val type = update.jobsSubscribe.type val jobData = update.jobsSubscribe.job.stashJob @@ -57,7 +64,7 @@ class PreferencesViewModel : ViewModel() { } } } - updateCacheUsage(context) + updateCacheUsage() } private suspend fun handleUpdate( @@ -102,7 +109,7 @@ class PreferencesViewModel : ViewModel() { } } - fun updateCacheUsage(context: Context) { + fun updateCacheUsage() { val networkDisk = Constants.getNetworkCache(context).size() val imageUsedMemory = context.imageLoader.memoryCache?.size ?: 0L val imageMaxMemory = context.imageLoader.memoryCache?.maxSize ?: 0L @@ -116,6 +123,20 @@ class PreferencesViewModel : ViewModel() { ) } + fun onTriggerScan() { + viewModelScope.launchDefault { + // TODO track status + mutationEngine.triggerScan() + } + } + + fun onTriggerGenerate() { + viewModelScope.launchDefault { + // TODO track status + mutationEngine.triggerGenerate() + } + } + companion object { private const val TAG = "PreferencesViewModel" } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServers.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServers.kt index dc0e7574..62744319 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServers.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServers.kt @@ -19,6 +19,7 @@ import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -40,7 +41,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.material3.Icon import androidx.tv.material3.ListItemDefaults import androidx.tv.material3.MaterialTheme @@ -54,18 +54,19 @@ import com.github.damontecres.stashapp.ui.components.filter.SimpleListItem import com.github.damontecres.stashapp.ui.tryRequestFocus import com.github.damontecres.stashapp.ui.util.ifElse import com.github.damontecres.stashapp.util.StashServer +import org.koin.androidx.compose.koinViewModel @Composable fun ManageServers( - currentServer: StashServer?, - onSwitchServer: (StashServer) -> Unit, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, - viewModel: ManageServersViewModel = viewModel(), + viewModel: ManageServersViewModel = koinViewModel(), ) { + val currentServer by viewModel.currentServer.collectAsState() val allServers by viewModel.allServers.observeAsState(listOf()) val serverStatus by viewModel.serverStatus.observeAsState(mapOf()) - val serversWithOutCurrent = allServers.toMutableList().apply { remove(currentServer) } + val serversWithOutCurrent = + remember { allServers.toMutableList().apply { remove(currentServer.server) } } var showAddServer by remember { mutableStateOf(false) } var showServerDialog by remember { mutableStateOf(null) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServersViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServersViewModel.kt index 0d7f743b..0801ab39 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServersViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/server/ManageServersViewModel.kt @@ -1,21 +1,23 @@ package com.github.damontecres.stashapp.ui.components.server +import android.app.Application import android.util.Log import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.apollographql.apollo.ApolloClient -import com.apollographql.apollo.network.http.DefaultHttpEngine import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.CredentialsQuery import com.github.damontecres.stashapp.api.GenerateApiKeyMutation -import com.github.damontecres.stashapp.util.MutationEngine -import com.github.damontecres.stashapp.util.QueryEngine +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.server.StashApi +import com.github.damontecres.stashapp.di.server.StashServer 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.TestResult import com.github.damontecres.stashapp.util.isNotNullOrBlank +import com.github.damontecres.stashapp.util.launchIO import com.github.damontecres.stashapp.util.testStashConnection import com.github.damontecres.stashapp.views.models.EqualityMutableLiveData import kotlinx.coroutines.CancellationException @@ -26,18 +28,27 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.FormBody import okhttp3.Request +import org.koin.core.annotation.KoinViewModel -class ManageServersViewModel : ViewModel() { +@KoinViewModel +class ManageServersViewModel( + private val context: Application, + private val api: StashApi, + private val serverRepository: ServerRepository, +) : ViewModel() { + val currentServer get() = serverRepository.currentServer val allServers = MutableLiveData>(listOf()) val serverStatus = MutableLiveData>(mapOf()) val connectionState = EqualityMutableLiveData(ConnectionState.Inactive) init { - val servers = StashServer.getAll(StashApplication.getApplication()) - allServers.value = servers - serverStatus.value = servers.associateWith { ServerTestResult.Pending } - viewModelScope.launch(StashCoroutineExceptionHandler()) { + viewModelScope.launchIO { + val servers = serverRepository.getAll() + withContext(Dispatchers.Main) { + allServers.value = servers + serverStatus.value = servers.associateWith { ServerTestResult.Pending } + } servers.forEach { server -> testServer(server) } @@ -52,11 +63,12 @@ class ManageServersViewModel : ViewModel() { viewModelScope.launch { serverStatus.value = serverStatus.value!!.toMutableMap().apply { put(server, ServerTestResult.Pending) } + val apolloClient = api.createApolloClient(server) val result = testStashConnection( - StashApplication.getApplication(), + context, false, - server.apolloClient, + apolloClient, ) val testResult = when (result) { @@ -75,23 +87,35 @@ class ManageServersViewModel : ViewModel() { } fun removeServer(server: StashServer) { - StashServer.removeStashServer(StashApplication.getApplication(), server) - val servers = StashServer.getAll(StashApplication.getApplication()) - allServers.value = servers - serverStatus.value = serverStatus.value!!.toMutableMap().apply { remove(server) } + viewModelScope.launchIO { + serverRepository.removeStashServer(server) + val servers = serverRepository.getAll() + withContext(Dispatchers.Main) { + allServers.value = servers + serverStatus.value = serverStatus.value!!.toMutableMap().apply { remove(server) } + } + } } fun addServer(server: StashServer) { - StashServer.addServer(StashApplication.getApplication(), server) - val servers = StashServer.getAll(StashApplication.getApplication()) - allServers.value = servers + viewModelScope.launchIO { + serverRepository.addServer(server) + val servers = serverRepository.getAll() + withContext(Dispatchers.Main) { + allServers.value = servers + } + } } fun addServerAsCurrent(server: StashServer) { - StashServer.addServer(StashApplication.getApplication(), server) - StashServer.setCurrentStashServer(StashApplication.getApplication(), server) - val servers = StashServer.getAll(StashApplication.getApplication()) - allServers.value = servers + viewModelScope.launchIO { + serverRepository.addServer(server) + serverRepository.setCurrentStashServer(server) + val servers = serverRepository.getAll() + withContext(Dispatchers.Main) { + allServers.value = servers + } + } } private var testServerJob: Job? = null @@ -119,10 +143,8 @@ class ManageServersViewModel : ViewModel() { testWithUsername(serverUrl, username, apiKey, trustCerts) } else { val apolloClient = - StashClient.createTestApolloClient( - context, + api.createApolloClient( StashServer(serverUrl, apiKey?.ifBlank { null }), - trustCerts, ) val result = testStashConnection(context, false, apolloClient) if (result is TestResult.Error && result.exception is CancellationException) { @@ -155,6 +177,7 @@ class ManageServersViewModel : ViewModel() { trustCerts: Boolean, ) { try { + TODO() val httpClient = StashClient.createCookieHttpClient(trustCerts) val loginUrl = StashClient.createLoginUrl(serverUrl) val request = @@ -175,14 +198,9 @@ class ManageServersViewModel : ViewModel() { if (!response.isSuccessful) { connectionState.value = ConnectionState.Result(TestResult.AuthRequired) } else { - val apolloClient = - ApolloClient - .Builder() - .serverUrl(StashClient.cleanServerUrl(serverUrl)) - .httpEngine(DefaultHttpEngine(httpClient)) - .build() - val queryEngine = QueryEngine(apolloClient, null) - val mutationEngine = MutationEngine(apolloClient, null) + val testApi = api.createFor(StashServer(serverUrl, null)) + val queryEngine = QueryEngine(testApi) + val mutationEngine = MutationEngine(testApi, serverRepository) val res = queryEngine.executeQuery(CredentialsQuery()) var currentApiKey = diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt index d77435f5..e576e0df 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt @@ -149,6 +149,7 @@ fun ApplicationContent( if (fullScreen) { DestinationContent( currentServer = currentServer, + navManger = navigationManager, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt index 79f493ea..7c7f7428 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.text.AnnotatedString import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.di.server.StashServer +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.components.ItemOnClicker @@ -40,6 +41,7 @@ import kotlin.time.Duration.Companion.seconds @Composable fun DestinationContent( currentServer: CurrentServer, + navManager: NavigationManager, destination: Destination, composeUiConfig: ComposeUiConfig, itemOnClick: ItemOnClicker, @@ -95,8 +97,6 @@ fun DestinationContent( is Destination.Settings -> { SettingsPage( - server = server, - navigationManager = navManager, preferenceScreenOption = destination.screenOption, uiConfig = composeUiConfig, onUpdateTitle = onUpdateTitle, @@ -106,8 +106,6 @@ fun DestinationContent( is Destination.ManageServers -> { ManageServers( - currentServer = server, - onSwitchServer = onSwitchServer, onUpdateTitle = onUpdateTitle, modifier = modifier, ) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt index 4d5332c4..1b529d5d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt @@ -205,6 +205,7 @@ fun NavDrawer( ) { DestinationContent( currentServer = currentServer, + navManger = navigationManager, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt index d66d2b92..0ae9ac8b 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt @@ -183,6 +183,7 @@ fun NavScaffold( ) { DestinationContent( currentServer = currentServer, + navManger = navigationManager, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt index 4ce9035e..c5a84ffa 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt @@ -43,7 +43,6 @@ import com.github.damontecres.stashapp.ui.FilterViewModel import com.github.damontecres.stashapp.ui.components.CircularProgress import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.playback.PlaybackPageContent -import com.github.damontecres.stashapp.ui.util.OneTimeLaunchedEffect import com.github.damontecres.stashapp.util.AlphabetSearchUtils import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler import com.github.damontecres.stashapp.util.SkipParams @@ -51,21 +50,24 @@ import com.github.damontecres.stashapp.util.StashServer import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @Composable fun PlaybackPage( - server: StashServer, uiConfig: ComposeUiConfig, sceneId: String, startPosition: Long, playbackMode: PlaybackMode, itemOnClick: ItemOnClicker, modifier: Modifier = Modifier, - viewModel: PlaybackPageViewModel = viewModel(), + viewModel: PlaybackPageViewModel = + koinViewModel { + parametersOf(sceneId) + }, ) { - OneTimeLaunchedEffect { viewModel.init(server, sceneId) } val state by viewModel.state.collectAsState() val context = LocalContext.current @@ -80,6 +82,7 @@ fun PlaybackPage( state?.let { state -> val player = remember { + TODO() val skipParams = uiConfig.preferences.playbackPreferences.let { SkipParams.Values( @@ -128,7 +131,7 @@ fun PlaybackPage( markersEnabled = true, playlistPager = null, modifier = - Modifier + modifier .fillMaxSize() .background(Color.Transparent), controlsEnabled = true, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt index bcd2f79e..b0f949a2 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt @@ -4,36 +4,48 @@ import android.util.Log import android.widget.Toast import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.fragment.FullSceneData import com.github.damontecres.stashapp.data.Scene -import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.QueryEngine -import com.github.damontecres.stashapp.util.StashServer +import com.github.damontecres.stashapp.di.server.QueryEngine +import com.github.damontecres.stashapp.di.services.ServerLogger +import com.github.damontecres.stashapp.util.launchIO +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel +import kotlin.coroutines.CoroutineContext -class PlaybackPageViewModel : ViewModel() { - private lateinit var server: StashServer - private lateinit var sceneId: String +@KoinViewModel +class PlaybackPageViewModel( + private val serverLogger: ServerLogger, + private val queryEngine: QueryEngine, + @InjectedParam private val sceneId: String, +) : ViewModel() { + private val exceptionHandler = + object : CoroutineExceptionHandler { + override val key: CoroutineContext.Key<*> + get() = CoroutineExceptionHandler + + override fun handleException( + context: CoroutineContext, + exception: Throwable, + ) { + Logger.e(exception) { "Exception" } + viewModelScope.launchIO { + serverLogger.logException(exception, null) + } + } + } val state = MutableStateFlow(null) - fun init( - server: StashServer, - sceneId: String, - ) { - this.server = server - this.sceneId = sceneId + init { Log.d("PlaybackViewModel", "scene=$sceneId") - viewModelScope.launch( - LoggingCoroutineExceptionHandler( - server, - viewModelScope, - toastMessage = "Error fetching scene", - ), - ) { - val fullScene = QueryEngine(server).getScene(sceneId) + viewModelScope.launch(exceptionHandler) { + val fullScene = queryEngine.getScene(sceneId) if (fullScene != null) { val scene = Scene.fromFullSceneData(fullScene) state.value = PlaybackState(fullScene, scene) diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SettingsPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SettingsPage.kt index 9cf85f24..a8a2dff7 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SettingsPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SettingsPage.kt @@ -10,17 +10,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.AnnotatedString import androidx.tv.material3.MaterialTheme import com.github.damontecres.stashapp.PreferenceScreenOption -import com.github.damontecres.stashapp.navigation.NavigationManager import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.compat.isTvDevice import com.github.damontecres.stashapp.ui.components.prefs.PreferencesContent -import com.github.damontecres.stashapp.util.StashServer @SuppressLint("RestrictedApi") @Composable fun SettingsPage( - server: StashServer, - navigationManager: NavigationManager, preferenceScreenOption: PreferenceScreenOption, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, @@ -38,8 +34,6 @@ fun SettingsPage( Modifier } PreferencesContent( - server, - navigationManager, uiConfig, preferenceScreenOption, newModifier, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/UpdateAppPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/UpdateAppPage.kt index 3a9abb9f..7a03271a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/UpdateAppPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/UpdateAppPage.kt @@ -49,7 +49,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation -import com.github.damontecres.stashapp.navigation.NavigationManager +import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.Material3AppTheme import com.github.damontecres.stashapp.ui.compat.Button From 4799f0b50677c30af6401f742b0f729e75265b2f Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 17:33:51 -0400 Subject: [PATCH 05/25] Checkpoint 3 --- .../stashapp/filter/CreateFilterViewModel.kt | 55 ++++---- .../stashapp/ui/FilterViewModel.kt | 41 +++--- .../stashapp/ui/components/StashGrid.kt | 2 - .../ui/components/filter/CreateFilter.kt | 31 +++-- .../components/image/ImageDetailsViewModel.kt | 125 ++++++++++-------- .../ui/components/image/ImageOverlay.kt | 4 +- .../stashapp/ui/nav/DestinationContent.kt | 14 +- .../stashapp/ui/pages/ChooseThemePage.kt | 34 +---- .../stashapp/ui/pages/DebugPage.kt | 22 ++- .../stashapp/ui/pages/FilterPage.kt | 19 +-- .../stashapp/ui/pages/ImagePage.kt | 22 ++- .../stashapp/ui/pages/MarkerTimestampPage.kt | 20 +-- .../stashapp/ui/pages/PlaybackPage.kt | 26 ++-- .../ui/pages/PlaybackPageViewModel.kt | 3 + .../util/LoggingCoroutineExceptionHandler.kt | 7 +- .../views/models/MarkerDetailsViewModel.kt | 32 ++--- 16 files changed, 222 insertions(+), 235 deletions(-) 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 c0d13579..227692ff 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.asUtilQueryEngine(), + 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/ui/FilterViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterViewModel.kt index d368f635..944e444d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterViewModel.kt @@ -8,19 +8,26 @@ import com.apollographql.apollo.api.Query import com.github.damontecres.stashapp.api.fragment.StashData import com.github.damontecres.stashapp.api.type.SortDirectionEnum import com.github.damontecres.stashapp.data.DataType +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.suppliers.DataSupplierFactory import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.suppliers.StashPagingSource import com.github.damontecres.stashapp.util.AlphabetSearchUtils import com.github.damontecres.stashapp.util.ComposePager -import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.QueryEngine -import com.github.damontecres.stashapp.util.StashServer +import com.github.damontecres.stashapp.util.launchIO +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.annotation.KoinViewModel -class FilterViewModel : ViewModel() { - private var server: StashServer? = null +@KoinViewModel +class FilterViewModel( + private val serverRepository: ServerRepository, + private val queryEngine: QueryEngine, + val navigationManager: NavigationManager, +) : ViewModel() { val pager = MutableLiveData>() val currentFilter: FilterArgs? get() = pager.value?.filter @@ -29,39 +36,41 @@ class FilterViewModel : ViewModel() { private var job: Job? = null fun setFilter( - server: StashServer, filterArgs: FilterArgs, columns: Int, ) { - if (pager.value?.filter != filterArgs || server != this.server) { + if (pager.value?.filter != filterArgs) { job?.cancel() Log.d("FilterPageViewModel", "filterArgs=$filterArgs, columns=$columns") - this.server = server - val dataSupplierFactory = DataSupplierFactory(server.version) + val dataSupplierFactory = DataSupplierFactory(serverRepository.currentServerVersion) val dataSupplier = dataSupplierFactory.create(filterArgs) val pagingSource = - StashPagingSource(QueryEngine(server), dataSupplier) { _, _, item -> item } + StashPagingSource( + queryEngine.asUtilQueryEngine(), + dataSupplier, + ) { _, _, item -> item } val pager = ComposePager(filterArgs, pagingSource, viewModelScope, pageSize = columns * 10) job = - viewModelScope.launch(LoggingCoroutineExceptionHandler(server, viewModelScope)) { + viewModelScope.launchIO { pager.init() - this@FilterViewModel.pager.value = pager + withContext(Dispatchers.IO) { + this@FilterViewModel.pager.value = pager + } } } } suspend fun findLetterPosition(letter: Char): Int { - val server = this.server!! val filter = this.pager.value!!.filter - val dataSupplierFactory = DataSupplierFactory(server.version) + val dataSupplierFactory = DataSupplierFactory(serverRepository.currentServerVersion) val letterPosition = AlphabetSearchUtils.findPosition( letter, filter, - QueryEngine(server), + queryEngine.asUtilQueryEngine(), dataSupplierFactory, ) val jumpPosition = diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/StashGrid.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/StashGrid.kt index cfe1cf6e..27258432 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/StashGrid.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/StashGrid.kt @@ -79,7 +79,6 @@ import com.github.damontecres.stashapp.ui.util.ifElse import com.github.damontecres.stashapp.util.AlphabetSearchUtils import com.github.damontecres.stashapp.util.ComposePager import com.github.damontecres.stashapp.util.StashCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.defaultCardWidth import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -102,7 +101,6 @@ enum class CreateFilter { @OptIn(ExperimentalComposeUiApi::class) @Composable fun StashGridControls( - server: StashServer, pager: ComposePager, updateFilter: (FilterArgs) -> Unit, itemOnClick: ItemOnClicker, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/filter/CreateFilter.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/filter/CreateFilter.kt index 021cd11c..5e756c41 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/filter/CreateFilter.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/filter/CreateFilter.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -43,7 +44,6 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.apollographql.apollo.api.Optional @@ -58,15 +58,15 @@ import com.github.damontecres.stashapp.filter.FilterOption import com.github.damontecres.stashapp.filter.filterSummary import com.github.damontecres.stashapp.filter.getFilterOptions import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManager import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig -import com.github.damontecres.stashapp.ui.LocalGlobalContext import com.github.damontecres.stashapp.ui.components.CircularProgress import com.github.damontecres.stashapp.ui.tryRequestFocus import com.github.damontecres.stashapp.ui.util.ifElse import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf import kotlin.reflect.full.createInstance internal const val TAG = "CreateFilter" @@ -76,12 +76,14 @@ fun CreateFilterScreen( uiConfig: ComposeUiConfig, dataType: DataType, initialFilter: FilterArgs?, - navigationManager: NavigationManager, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, - viewModel: CreateFilterViewModel = viewModel(), + viewModel: CreateFilterViewModel = + koinViewModel { + parametersOf(dataType, initialFilter) + }, ) { - val server = LocalGlobalContext.current.server + val currentServer by viewModel.currentServer.collectAsState() val scope = rememberCoroutineScope() CreateFilterContent( uiConfig = uiConfig, @@ -92,18 +94,18 @@ fun CreateFilterScreen( if (save) { scope.launch( LoggingCoroutineExceptionHandler( - server, + currentServer, scope, toastMessage = "Error saving filter", ), ) { viewModel.saveFilter() - navigationManager.goBack() - navigationManager.navigate(Destination.Filter(filterArgs)) + viewModel.navigationManager.goBack() + viewModel.navigationManager.navigate(Destination.Filter(filterArgs)) } } else { - navigationManager.goBack() - navigationManager.navigate(Destination.Filter(filterArgs)) + viewModel.navigationManager.goBack() + viewModel.navigationManager.navigate(Destination.Filter(filterArgs)) } }, modifier = modifier, @@ -121,7 +123,10 @@ fun CreateFilterContent( onSubmit: (save: Boolean, filter: FilterArgs) -> Unit, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, - viewModel: CreateFilterViewModel = viewModel(), + viewModel: CreateFilterViewModel = + koinViewModel { + parametersOf(dataType, initialFilter) + }, ) { val context = LocalContext.current @@ -135,7 +140,7 @@ fun CreateFilterContent( LaunchedEffect(title) { onUpdateTitle?.invoke(AnnotatedString(title)) } LaunchedEffect(initialFilter) { - viewModel.initialize(dataType, initialFilter) + viewModel.initialize() viewModel.updateCount() } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageDetailsViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageDetailsViewModel.kt index 2b8fe834..666695f8 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageDetailsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageDetailsViewModel.kt @@ -23,16 +23,18 @@ import com.github.damontecres.stashapp.data.OCounter import com.github.damontecres.stashapp.data.ThrottledLiveData import com.github.damontecres.stashapp.data.VideoFilter import com.github.damontecres.stashapp.data.room.PlaybackEffect +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.suppliers.DataSupplierFactory import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.suppliers.StashPagingSource import com.github.damontecres.stashapp.ui.galleryId import com.github.damontecres.stashapp.util.ComposePager import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -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.isImageClip import com.github.damontecres.stashapp.util.launchIO import com.github.damontecres.stashapp.util.showSetRatingToast @@ -43,11 +45,21 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel import timber.log.Timber import kotlin.properties.Delegates -class ImageDetailsViewModel : ViewModel() { - private var server: StashServer? = null +@KoinViewModel +class ImageDetailsViewModel( + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + private val queryEngine: QueryEngine, + private val mutationEngine: MutationEngine, + val navigationManager: NavigationManager, + @InjectedParam private val filterArgs: FilterArgs, + @InjectedParam private val startPosition: Int, +) : ViewModel() { private var saveFilters = true private lateinit var exceptionHandler: LoggingCoroutineExceptionHandler @@ -94,9 +106,6 @@ class ImageDetailsViewModel : ViewModel() { val galleryId: LiveData = _galleryId fun init( - server: StashServer, - filterArgs: FilterArgs, - startPosition: Int, slideshow: Boolean, slideshowDelay: Long, saveFilters: Boolean, @@ -104,26 +113,33 @@ class ImageDetailsViewModel : ViewModel() { Log.v(TAG, "View model init") this.saveFilters = saveFilters this.slideshowDelay = slideshowDelay - if (pager.value?.filter != filterArgs || server != this.server) { + if (pager.value?.filter != filterArgs) { if (filterArgs.dataType != DataType.IMAGE) { throw IllegalArgumentException("Cannot use ${filterArgs.dataType}") } _galleryId.value = (filterArgs.objectFilter as? ImageFilterType)?.galleryId - this.server = server this.exceptionHandler = LoggingCoroutineExceptionHandler( - server, + serverRepository.currentServer.value, viewModelScope, toastMessage = "Error updating image", ) - val dataSupplierFactory = DataSupplierFactory(server.version) + val dataSupplierFactory = DataSupplierFactory(serverRepository.currentServerVersion) val dataSupplier = dataSupplierFactory.create(filterArgs) val pagingSource = - StashPagingSource(QueryEngine(server), dataSupplier) { _, _, item -> item } + StashPagingSource( + queryEngine.asUtilQueryEngine(), + dataSupplier, + ) { _, _, item -> item } val pager = ComposePager(filterArgs, pagingSource, viewModelScope) Log.v(TAG, "Pager created: filterArgs=$filterArgs") - viewModelScope.launch(LoggingCoroutineExceptionHandler(server, viewModelScope)) { + viewModelScope.launch( + LoggingCoroutineExceptionHandler( + serverRepository.currentServer.value, + viewModelScope, + ), + ) { pager.init() Log.v(TAG, "Pager size: ${pager.size}") this@ImageDetailsViewModel.pager.value = pager @@ -137,6 +153,7 @@ class ImageDetailsViewModel : ViewModel() { galleryId.value?.let { galleryId -> viewModelScope.launchIO { viewModelScope.launchIO(StashCoroutineExceptionHandler()) { + val server = serverRepository.currentServer.value.server val vf = StashApplication .getDatabase() @@ -192,7 +209,6 @@ class ImageDetailsViewModel : ViewModel() { Log.v(TAG, "Got image for $position: ${image != null}") if (image != null) { this@ImageDetailsViewModel.position.value = position - val queryEngine = QueryEngine(server!!) rating100.value = image.rating100 ?: 0 oCount.value = image.o_counter ?: 0 tags.value = listOf() @@ -202,6 +218,7 @@ class ImageDetailsViewModel : ViewModel() { updateImageFilter(galleryImageFilter) if (saveFilters) { viewModelScope.launchIO(StashCoroutineExceptionHandler()) { + val server = serverRepository.currentServer.value.server val vf = StashApplication .getDatabase() @@ -263,7 +280,7 @@ class ImageDetailsViewModel : ViewModel() { } catch (ex: Exception) { loadingState.value = ImageLoadingState.Error LoggingCoroutineExceptionHandler( - server!!, + serverRepository.currentServer.value, viewModelScope, toastMessage = "Error fetching image", ).handleException(ex) @@ -291,7 +308,6 @@ class ImageDetailsViewModel : ViewModel() { val mutable = it.toMutableList() mutator.invoke(mutable) viewModelScope.launch(exceptionHandler) { - val mutationEngine = MutationEngine(server!!) val result = mutationEngine.updateImage(imageId = imageId, tagIds = mutable) if (result != null) { tags.value = result.tags.map { it.tagData } @@ -319,7 +335,6 @@ class ImageDetailsViewModel : ViewModel() { val mutable = it.toMutableList() mutator.invoke(mutable) viewModelScope.launch(exceptionHandler) { - val mutationEngine = MutationEngine(server!!) val result = mutationEngine.updateImage(imageId = imageId, performerIds = mutable) if (result != null) { performers.value = result.performers.map { it.performerData } @@ -333,7 +348,6 @@ class ImageDetailsViewModel : ViewModel() { rating100: Int, ) { viewModelScope.launch(exceptionHandler) { - val mutationEngine = MutationEngine(server!!) val newRating = mutationEngine.updateImage(imageId, rating100 = rating100)?.rating100 ?: 0 this@ImageDetailsViewModel.rating100.value = newRating @@ -343,7 +357,6 @@ class ImageDetailsViewModel : ViewModel() { fun updateOCount(action: suspend MutationEngine.(String) -> OCounter) { viewModelScope.launch(exceptionHandler) { - val mutationEngine = MutationEngine(server!!) val newOCount = action.invoke(mutationEngine, _image.value!!.id) oCount.value = newOCount.count } @@ -404,24 +417,23 @@ class ImageDetailsViewModel : ViewModel() { } fun saveImageFilter() { - if (server != null) { - image.value?.let { - viewModelScope.launchIO(StashCoroutineExceptionHandler(autoToast = true)) { - val vf = _imageFilter.value - if (vf != null) { - StashApplication - .getDatabase() - .playbackEffectsDao() - .insert(PlaybackEffect(server!!.url, it.id, DataType.IMAGE, vf)) - Log.d(TAG, "Saved VideoFilter for image ${it.id}") - withContext(Dispatchers.Main) { - Toast - .makeText( - StashApplication.getApplication(), - "Saved", - Toast.LENGTH_SHORT, - ).show() - } + image.value?.let { + viewModelScope.launchIO(StashCoroutineExceptionHandler(autoToast = true)) { + val server = serverRepository.currentServer.value.server + val vf = _imageFilter.value + if (vf != null) { + StashApplication + .getDatabase() + .playbackEffectsDao() + .insert(PlaybackEffect(server!!.url, it.id, DataType.IMAGE, vf)) + Log.d(TAG, "Saved VideoFilter for image ${it.id}") + withContext(Dispatchers.Main) { + Toast + .makeText( + StashApplication.getApplication(), + "Saved", + Toast.LENGTH_SHORT, + ).show() } } } @@ -429,25 +441,24 @@ class ImageDetailsViewModel : ViewModel() { } fun saveGalleryFilter() { - if (server != null) { - galleryId.value?.let { galleryId -> - viewModelScope.launchIO(StashCoroutineExceptionHandler(autoToast = true)) { - val vf = _imageFilter.value - if (vf != null) { - galleryImageFilter = vf - StashApplication - .getDatabase() - .playbackEffectsDao() - .insert(PlaybackEffect(server!!.url, galleryId, DataType.GALLERY, vf)) - Log.d(TAG, "Saved VideoFilter for gallery $galleryId") - withContext(Dispatchers.Main) { - Toast - .makeText( - StashApplication.getApplication(), - "Saved", - Toast.LENGTH_SHORT, - ).show() - } + galleryId.value?.let { galleryId -> + viewModelScope.launchIO(StashCoroutineExceptionHandler(autoToast = true)) { + val server = serverRepository.currentServer.value.server + val vf = _imageFilter.value + if (vf != null) { + galleryImageFilter = vf + StashApplication + .getDatabase() + .playbackEffectsDao() + .insert(PlaybackEffect(server!!.url, galleryId, DataType.GALLERY, vf)) + Log.d(TAG, "Saved VideoFilter for gallery $galleryId") + withContext(Dispatchers.Main) { + Toast + .makeText( + StashApplication.getApplication(), + "Saved", + Toast.LENGTH_SHORT, + ).show() } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageOverlay.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageOverlay.kt index a4bbf755..35b0d028 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageOverlay.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/image/ImageOverlay.kt @@ -32,6 +32,7 @@ import com.github.damontecres.stashapp.api.fragment.StashData 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.di.server.MutationEngine import com.github.damontecres.stashapp.filter.extractTitle import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.compat.isNotTvDevice @@ -44,13 +45,10 @@ import com.github.damontecres.stashapp.ui.pages.DialogParams import com.github.damontecres.stashapp.ui.pages.SearchForDialog import com.github.damontecres.stashapp.ui.pages.SearchForParams import com.github.damontecres.stashapp.ui.titleCount -import com.github.damontecres.stashapp.util.MutationEngine -import com.github.damontecres.stashapp.util.StashServer @androidx.annotation.OptIn(UnstableApi::class) @Composable fun ImageOverlay( - server: StashServer, player: Player, slideshowControls: SlideshowControls, slideshowEnabled: Boolean, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt index 7c7f7428..d9b1916a 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt @@ -113,7 +113,6 @@ fun DestinationContent( is Destination.Playback -> { PlaybackPage( - server = server, sceneId = destination.sceneId, startPosition = destination.position, playbackMode = destination.mode, @@ -125,7 +124,7 @@ fun DestinationContent( is Destination.Playlist -> { PlaylistPlaybackPage( - server = server, + currentServer = currentServer, uiConfig = composeUiConfig, filterArgs = destination.filterArgs, startIndex = destination.position, @@ -137,8 +136,7 @@ fun DestinationContent( is Destination.Slideshow -> { ImagePage( - server = server, - navigationManager = navManager, + currentServer = currentServer, filter = destination.filterArgs, startPosition = destination.position, startSlideshow = destination.automatic, @@ -151,7 +149,6 @@ fun DestinationContent( Destination.ChooseTheme -> { ChooseThemePage( - server = server, navigationManager = navManager, uiConfig = composeUiConfig, onChooseTheme = onChangeTheme, @@ -164,7 +161,6 @@ fun DestinationContent( uiConfig = composeUiConfig, dataType = destination.dataType, initialFilter = destination.startingFilter, - navigationManager = navManager, onUpdateTitle = onUpdateTitle, modifier = modifier, ) @@ -172,8 +168,6 @@ fun DestinationContent( is Destination.UpdateMarker -> { MarkerTimestampPage( - server = server, - navigationManager = navManager, uiConfig = composeUiConfig, markerId = destination.markerId, modifier = Modifier.fillMaxSize(), @@ -182,7 +176,7 @@ fun DestinationContent( is Destination.Debug -> { DebugPage( - server = server, + currentServer = currentServer, uiConfig = composeUiConfig, modifier = Modifier.fillMaxSize(), ) @@ -198,8 +192,6 @@ fun DestinationContent( is Destination.Filter -> { FilterPage( - server = server, - navigationManager = navManager, initialFilter = destination.filterArgs, scrollToNextPage = destination.scrollToNextPage, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ChooseThemePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ChooseThemePage.kt index c5c1eda9..fc70b5f3 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ChooseThemePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ChooseThemePage.kt @@ -43,9 +43,7 @@ import androidx.tv.material3.Text import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.DataType -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.di.services.NavigationManager import com.github.damontecres.stashapp.presenters.ScenePresenter import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.PreviewTheme @@ -62,7 +60,6 @@ import com.github.damontecres.stashapp.ui.parseThemeJson import com.github.damontecres.stashapp.ui.readThemeJson import com.github.damontecres.stashapp.ui.uiConfigPreview 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.preferences import com.github.damontecres.stashapp.util.updateInterfacePreferences @@ -75,7 +72,6 @@ private const val TAG = "ChooseTheme" @Composable fun ChooseThemePage( - server: StashServer, navigationManager: NavigationManager, uiConfig: ComposeUiConfig, onChooseTheme: (String?) -> Unit, @@ -361,33 +357,7 @@ fun deleteJson( private fun ChooseThemePagePreview() { PreviewTheme { ChooseThemePage( - server = StashServer("0.0.0.0", null), - navigationManager = - object : NavigationManager { - override var previousDestination: Destination? - get() = TODO("Not yet implemented") - set(value) {} - - override fun navigate(destination: Destination) { - TODO("Not yet implemented") - } - - override fun goBack() { - TODO("Not yet implemented") - } - - override fun goToMain() { - TODO("Not yet implemented") - } - - override fun clearPinFragment() { - TODO("Not yet implemented") - } - - override fun addListener(listener: NavigationListener) { - TODO("Not yet implemented") - } - }, + navigationManager = NavigationManager(), uiConfig = uiConfigPreview, onChooseTheme = {}, modifier = Modifier, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/DebugPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/DebugPage.kt index 65a650a7..ad923f25 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/DebugPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/DebugPage.kt @@ -38,6 +38,7 @@ import com.github.damontecres.stashapp.BuildConfig import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.room.PlaybackEffect +import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.playback.CodecSupport import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.components.TableRow @@ -45,7 +46,6 @@ import com.github.damontecres.stashapp.ui.components.TableRowComposable import com.github.damontecres.stashapp.ui.tryRequestFocus 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 com.github.damontecres.stashapp.util.toReadableString import kotlinx.coroutines.Dispatchers @@ -75,10 +75,11 @@ fun TableRowSmall( @Composable fun DebugPage( - server: StashServer, + currentServer: CurrentServer, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, ) { + val server = currentServer.server val context = LocalContext.current val columnState = rememberLazyListState() val scope = rememberCoroutineScope() @@ -199,21 +200,18 @@ fun DebugPage( TableRowSmall(it) } - val rows = - server.serverPreferences.preferences.all.let { prefs -> - prefs.keys.sorted().mapNotNull { - tableRow(it, prefs[it]) - } - } + val rows = currentServer.serverPreferences.toReadableString(true) Column { Text( text = "Server Preferences", style = MaterialTheme.typography.displaySmall, color = MaterialTheme.colorScheme.onBackground, ) - rows.forEach { row -> - TableRowSmall(row) - } + Text( + text = rows, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onBackground, + ) Text( text = "Server default filters", style = MaterialTheme.typography.displaySmall, @@ -221,7 +219,7 @@ fun DebugPage( ) DataType.entries .mapNotNull { - val filter = server.serverPreferences.getDefaultFilter(it) + val filter = currentServer.serverPreferences.defaultFilters[it]!! val value = "findFilter=${filter.findFilter}\nobjectFilter=${filter.objectFilter?.toReadableString(true)}" TableRow.from(it.name, value) }.forEach { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/FilterPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/FilterPage.kt index 0ea82049..8ef69521 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/FilterPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/FilterPage.kt @@ -10,12 +10,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManagerCompose import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.FilterViewModel @@ -25,12 +23,10 @@ import com.github.damontecres.stashapp.ui.components.FilterUiMode import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.LongClicker import com.github.damontecres.stashapp.ui.components.StashGridControls -import com.github.damontecres.stashapp.util.StashServer +import org.koin.androidx.compose.koinViewModel @Composable fun FilterPage( - server: StashServer, - navigationManager: NavigationManagerCompose, initialFilter: FilterArgs, scrollToNextPage: Boolean, uiConfig: ComposeUiConfig, @@ -38,12 +34,12 @@ fun FilterPage( longClicker: LongClicker, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, - viewModel: FilterViewModel = viewModel(), + viewModel: FilterViewModel = koinViewModel(), ) { if (viewModel.currentFilter == null) { // If the view model is populated, don't do it again - LaunchedEffect(server, initialFilter) { - viewModel.setFilter(server, initialFilter, uiConfig.cardSettings.columns) + LaunchedEffect(initialFilter) { + viewModel.setFilter(initialFilter, uiConfig.cardSettings.columns) } } val pager by viewModel.pager.observeAsState() @@ -74,7 +70,6 @@ fun FilterPage( StashGridControls( modifier = Modifier, uiConfig = uiConfig, - server = server, pager = pager!!, filterUiMode = FilterUiMode.SAVED_FILTERS, createFilter = { @@ -82,7 +77,7 @@ fun FilterPage( val currentFilter = viewModel.currentFilter when (it) { CreateFilter.FROM_CURRENT -> { - navigationManager.navigate( + viewModel.navigationManager.navigate( Destination.CreateFilter( dataType, currentFilter, @@ -91,7 +86,7 @@ fun FilterPage( } CreateFilter.NEW_FILTER -> { - navigationManager.navigate( + viewModel.navigationManager.navigate( Destination.CreateFilter( dataType, null, @@ -104,7 +99,7 @@ fun FilterPage( longClicker = longClicker, initialPosition = initialPosition, updateFilter = { - viewModel.setFilter(server, it, uiConfig.cardSettings.columns) + viewModel.setFilter(it, uiConfig.cardSettings.columns) }, letterPosition = viewModel::findLetterPosition, requestFocus = true, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ImagePage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ImagePage.kt index 727da309..4569e806 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ImagePage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/ImagePage.kt @@ -50,7 +50,6 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LifecycleStartEffect -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi @@ -69,7 +68,7 @@ import com.github.damontecres.stashapp.StashExoPlayer import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.data.VideoFilter -import com.github.damontecres.stashapp.navigation.NavigationManagerCompose +import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.playback.maybeMuteAudio import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.AppColors @@ -88,12 +87,13 @@ import com.github.damontecres.stashapp.ui.components.playback.isDpad import com.github.damontecres.stashapp.ui.components.playback.isEnterKey import com.github.damontecres.stashapp.ui.tryRequestFocus import com.github.damontecres.stashapp.ui.util.ifElse -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.findActivity import com.github.damontecres.stashapp.util.isImageClip import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.keepScreenOn import com.github.damontecres.stashapp.util.maxFileSize +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf import kotlin.math.abs private const val TAG = "ImagePage" @@ -103,8 +103,7 @@ private const val DEBUG = false @OptIn(UnstableApi::class) @Composable fun ImagePage( - server: StashServer, - navigationManager: NavigationManagerCompose, + currentServer: CurrentServer, filter: FilterArgs, startPosition: Int, startSlideshow: Boolean, @@ -112,17 +111,17 @@ fun ImagePage( longClicker: LongClicker, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, - viewModel: ImageDetailsViewModel = viewModel(), + viewModel: ImageDetailsViewModel = + koinViewModel { + parametersOf(filter, startPosition) + }, ) { val context = LocalContext.current val isNotTvDevice = isNotTvDevice - LaunchedEffect(server, filter) { + LaunchedEffect(currentServer, filter) { val slideshowDelay = uiConfig.preferences.interfacePreferences.slideShowIntervalMs viewModel.init( - server, - filter, - startPosition, startSlideshow, slideshowDelay, uiConfig.persistVideoFilters, @@ -261,7 +260,7 @@ fun ImagePage( val player = remember { StashExoPlayer - .getInstance(context, server, uiConfig.preferences.playbackPreferences) + .getInstance(context, currentServer, uiConfig.preferences.playbackPreferences) .apply { maybeMuteAudio(uiConfig.preferences, false, this) repeatMode = Player.REPEAT_MODE_OFF @@ -589,7 +588,6 @@ fun ImagePage( contentModifier .fillMaxSize() .background(AppColors.TransparentBlack50), - server = server, player = player, slideshowControls = slideshowControls, slideshowEnabled = slideshowEnabled, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerTimestampPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerTimestampPage.kt index 526efc17..5caa086f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerTimestampPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerTimestampPage.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.asFlow -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.util.UnstableApi import androidx.media3.ui.compose.PlayerSurface import androidx.media3.ui.compose.SURFACE_TYPE_SURFACE_VIEW @@ -47,7 +46,6 @@ import com.github.damontecres.stashapp.StashExoPlayer import com.github.damontecres.stashapp.api.fragment.FullMarkerData import com.github.damontecres.stashapp.api.type.SceneMarkerUpdateInput import com.github.damontecres.stashapp.data.Scene -import com.github.damontecres.stashapp.navigation.NavigationManager import com.github.damontecres.stashapp.playback.CodecSupport import com.github.damontecres.stashapp.playback.PlaybackMode import com.github.damontecres.stashapp.playback.buildMediaItem @@ -59,9 +57,7 @@ import com.github.damontecres.stashapp.ui.components.CircularProgress import com.github.damontecres.stashapp.ui.components.SwitchWithLabel import com.github.damontecres.stashapp.ui.components.TimestampPicker import com.github.damontecres.stashapp.ui.tryRequestFocus -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.titleOrFilename import com.github.damontecres.stashapp.util.toLongMilliseconds import com.github.damontecres.stashapp.util.toSeconds @@ -70,6 +66,8 @@ import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.collectIndexed import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf import kotlin.time.Duration.Companion.seconds import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -80,14 +78,15 @@ private const val TAG = "MarkerTimestampPage" @OptIn(FlowPreview::class) @Composable fun MarkerTimestampPage( - server: StashServer, - navigationManager: NavigationManager, uiConfig: ComposeUiConfig, markerId: String, modifier: Modifier = Modifier, - viewModel: MarkerDetailsViewModel = viewModel(), + viewModel: MarkerDetailsViewModel = + koinViewModel { + parametersOf(markerId) + }, ) { - LaunchedEffect(markerId) { viewModel.init(server, markerId) } + LaunchedEffect(markerId) { viewModel.init() } val scope = rememberCoroutineScope() val marker by viewModel.item.observeAsState() @@ -268,7 +267,8 @@ fun MarkerTimestampPage( ), ) { try { - val mutationEngine = MutationEngine(server) + // TODO + val mutationEngine = viewModel.mutationEngine val newEnd = if (setEndTimestamp) { Optional.present(end.inWholeMilliseconds.toSeconds) @@ -293,7 +293,7 @@ fun MarkerTimestampPage( "newSeconds=${result?.seconds}, newEnd=${result?.end_seconds}", ) - navigationManager.goBack() + viewModel.navigationManager.goBack() } finally { saving = false } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt index c5a84ffa..1cc857d8 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player @@ -29,6 +28,7 @@ import com.github.damontecres.stashapp.api.type.IntCriterionInput import com.github.damontecres.stashapp.api.type.SceneFilterType import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.Scene +import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.playback.CodecSupport import com.github.damontecres.stashapp.playback.PlaybackMode import com.github.damontecres.stashapp.playback.PlaylistFragment @@ -46,7 +46,6 @@ import com.github.damontecres.stashapp.ui.components.playback.PlaybackPageConten import com.github.damontecres.stashapp.util.AlphabetSearchUtils import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler import com.github.damontecres.stashapp.util.SkipParams -import com.github.damontecres.stashapp.util.StashServer import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -70,6 +69,7 @@ fun PlaybackPage( ) { val state by viewModel.state.collectAsState() val context = LocalContext.current + val currentServer by viewModel.currentServer.collectAsState() val playbackMode = remember(playbackMode, uiConfig) { @@ -123,7 +123,7 @@ fun PlaybackPage( } PlaybackPageContent( - server = server, + currentServer = currentServer, player = player, playlist = listOf(media), startIndex = 0, @@ -179,24 +179,24 @@ const val PLAYLIST_PREFETCH = 15 @Composable fun PlaylistPlaybackPage( - server: StashServer, + currentServer: CurrentServer, uiConfig: ComposeUiConfig, filterArgs: FilterArgs, startIndex: Int, itemOnClick: ItemOnClicker, modifier: Modifier = Modifier, clipDuration: Duration = 30.seconds, - viewModel: FilterViewModel = viewModel(key = "main"), - playlistViewModel: FilterViewModel = viewModel(key = "playlist"), + viewModel: FilterViewModel = koinViewModel(key = "main"), + playlistViewModel: FilterViewModel = koinViewModel(key = "playlist"), ) { val scope = rememberCoroutineScope() Log.v("PlaybackPageContent", "startIndex=$startIndex") val context = LocalContext.current - LaunchedEffect(server, filterArgs) { + LaunchedEffect(currentServer, filterArgs) { // TODO switch to single query - viewModel.setFilter(server, adjustFilter(filterArgs), uiConfig.cardSettings.columns) - playlistViewModel.setFilter(server, filterArgs, uiConfig.cardSettings.columns) + viewModel.setFilter(adjustFilter(filterArgs), uiConfig.cardSettings.columns) + playlistViewModel.setFilter(filterArgs, uiConfig.cardSettings.columns) } val pager by viewModel.pager.observeAsState() // var playlist by remember(pager) { mutableStateOf>(listOf()) } @@ -227,7 +227,7 @@ fun PlaylistPlaybackPage( val player = remember { StashExoPlayer - .getInstance(context, server, uiConfig.preferences.playbackPreferences) + .getInstance(context, currentServer, uiConfig.preferences.playbackPreferences) .apply { repeatMode = Player.REPEAT_MODE_OFF playWhenReady = true @@ -241,7 +241,7 @@ fun PlaylistPlaybackPage( mediaItem: MediaItem?, reason: Int, ) { - scope.launch(LoggingCoroutineExceptionHandler(server, scope)) { + scope.launch(LoggingCoroutineExceptionHandler(currentServer, scope)) { mutex.withLock { val currentIndex = player.currentMediaItemIndex val count = player.mediaItemCount @@ -274,7 +274,7 @@ fun PlaylistPlaybackPage( } PlaybackPageContent( - server = server, + currentServer = currentServer, player = player, playlist = playlist, startIndex = startIndex, @@ -286,7 +286,7 @@ fun PlaylistPlaybackPage( if (index < player.mediaItemCount) { player.seekTo(index, C.TIME_UNSET) } else { - scope.launch(LoggingCoroutineExceptionHandler(server, scope)) { + scope.launch(LoggingCoroutineExceptionHandler(currentServer, scope)) { mutex.withLock { val count = player.mediaItemCount pager?.let { pager -> diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt index b0f949a2..23bc8118 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt @@ -9,6 +9,7 @@ import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.fragment.FullSceneData import com.github.damontecres.stashapp.data.Scene import com.github.damontecres.stashapp.di.server.QueryEngine +import com.github.damontecres.stashapp.di.server.ServerRepository import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.util.launchIO import kotlinx.coroutines.CoroutineExceptionHandler @@ -20,6 +21,7 @@ import kotlin.coroutines.CoroutineContext @KoinViewModel class PlaybackPageViewModel( + private val serverRepository: ServerRepository, private val serverLogger: ServerLogger, private val queryEngine: QueryEngine, @InjectedParam private val sceneId: String, @@ -41,6 +43,7 @@ class PlaybackPageViewModel( } val state = MutableStateFlow(null) + val currentServer get() = serverRepository.currentServer init { Log.d("PlaybackViewModel", "scene=$sceneId") diff --git a/app/src/main/java/com/github/damontecres/stashapp/util/LoggingCoroutineExceptionHandler.kt b/app/src/main/java/com/github/damontecres/stashapp/util/LoggingCoroutineExceptionHandler.kt index 79b9c8e4..a6b0c4ab 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/util/LoggingCoroutineExceptionHandler.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/util/LoggingCoroutineExceptionHandler.kt @@ -4,8 +4,8 @@ import android.util.Log import android.widget.Toast import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.StashApplication +import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.navigation.NavigationManagerCompose -import com.github.damontecres.stashapp.util.plugin.CompanionPlugin import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope @@ -14,7 +14,7 @@ import kotlinx.coroutines.runBlocking import kotlin.coroutines.CoroutineContext class LoggingCoroutineExceptionHandler( - private val server: StashServer, + private val server: CurrentServer, private val scope: CoroutineScope, private val showToast: Boolean = true, private val toastMessage: String = "Error", @@ -55,7 +55,8 @@ class LoggingCoroutineExceptionHandler( scope.launchIO { val message = "Exception: compose=$compose, destination=$destination\n${exception.stackTraceToString()}" - CompanionPlugin.sendLogMessage(server, message, true) + TODO() +// CompanionPlugin.sendLogMessage(server, message, true) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt index a1e72919..dc7b03cf 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt @@ -8,19 +8,29 @@ import com.apollographql.apollo.api.Optional import com.github.damontecres.stashapp.api.fragment.FullMarkerData import com.github.damontecres.stashapp.api.fragment.TagData import com.github.damontecres.stashapp.api.type.SceneMarkerUpdateInput +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.ui.showAddTag import com.github.damontecres.stashapp.ui.showShort -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.launch +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -class MarkerDetailsViewModel : ViewModel() { - private lateinit var server: StashServer - +@KoinViewModel +class MarkerDetailsViewModel( + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + private val queryEngine: QueryEngine, + val mutationEngine: MutationEngine, + val navigationManager: NavigationManager, + @InjectedParam private val id: String, +) : ViewModel() { val seconds = MutableLiveData() val endSeconds = MutableLiveData(null) @@ -36,13 +46,8 @@ class MarkerDetailsViewModel : ViewModel() { private val _primaryTag = EqualityMutableLiveData() val primaryTag: LiveData = _primaryTag - fun init( - server: StashServer, - id: String, - ) { - this.server = server + fun init() { viewModelScope.launch(StashCoroutineExceptionHandler(true)) { - val queryEngine = QueryEngine(server) val marker = queryEngine.getMarker(id) _item.value = marker if (marker != null) { @@ -62,7 +67,6 @@ class MarkerDetailsViewModel : ViewModel() { fun setPrimaryTag(tagId: String) { viewModelScope.launch { - val mutationEngine = MutationEngine(server) val result = mutationEngine.updateMarker( SceneMarkerUpdateInput( @@ -81,7 +85,6 @@ class MarkerDetailsViewModel : ViewModel() { fun addTag(tagId: String) { viewModelScope.launch { - val mutationEngine = MutationEngine(server) val tagIds = tags.value!!.map { it.id }.toMutableList() tagIds.add(tagId) val result = @@ -102,7 +105,6 @@ class MarkerDetailsViewModel : ViewModel() { fun removeTag(tagId: String) { viewModelScope.launch { - val mutationEngine = MutationEngine(server) val tagIds = tags.value!!.map { it.id }.toMutableList() if (tagIds.remove(tagId)) { val result = From 3aa4a0948cc8baa5b4904f662cf58f5ce022db4b Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 18:17:20 -0400 Subject: [PATCH 06/25] Checkpoint 4 --- .../stashapp/di/services/ItemClicker.kt | 5 +- .../ui/components/CircularProgress.kt | 36 + .../stashapp/ui/components/ErrorMessage.kt | 58 ++ .../stashapp/ui/components/TabPage.kt | 17 +- .../stashapp/ui/nav/DestinationContent.kt | 16 - .../stashapp/ui/pages/GalleryPage.kt | 359 +++++--- .../stashapp/ui/pages/GroupPage.kt | 561 ++++++------ .../stashapp/ui/pages/MarkerPage.kt | 19 +- .../stashapp/ui/pages/PerformerPage.kt | 157 ++-- .../stashapp/ui/pages/SearchPage.kt | 30 +- .../stashapp/ui/pages/StudioPage.kt | 812 ++++++++++-------- .../damontecres/stashapp/ui/pages/TagPage.kt | 637 ++++++++------ .../views/models/MarkerDetailsViewModel.kt | 2 + 13 files changed, 1527 insertions(+), 1182 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/components/ErrorMessage.kt 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 index 4a6bca43..bd80b9f7 100644 --- 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 @@ -8,14 +8,15 @@ 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, -) { - fun onClick( +) : ItemOnClicker { + override fun onClick( item: Any, filterAndPosition: FilterAndPosition?, ) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/CircularProgress.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/CircularProgress.kt index 7a8e6cb2..bfc76f24 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/CircularProgress.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/CircularProgress.kt @@ -1,13 +1,21 @@ package com.github.damontecres.stashapp.ui.components +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.unit.dp import androidx.tv.material3.MaterialTheme import com.github.damontecres.stashapp.ui.Material3AppTheme +import com.github.damontecres.stashapp.ui.tryRequestFocus import com.github.damontecres.stashapp.ui.util.ifElse @Composable @@ -24,3 +32,31 @@ fun CircularProgress( } } } + +/** + * Fill the space with a loading indicator and take focus + */ +@Composable +fun LoadingPage( + modifier: Modifier = Modifier, + focusEnabled: Boolean = true, +) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + Box( + contentAlignment = Alignment.Center, + modifier = + modifier + .fillMaxSize() + .focusRequester(focusRequester) + .focusable(focusEnabled), + ) { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.border, + modifier = + Modifier + .align(Alignment.Center) + .size(48.dp), + ) + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/ErrorMessage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/ErrorMessage.kt new file mode 100644 index 00000000..c467efe1 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/ErrorMessage.kt @@ -0,0 +1,58 @@ +package com.github.damontecres.stashapp.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import com.github.damontecres.stashapp.R + +/** + * Displays an error message and/or exception + */ +@Composable +fun ErrorMessage( + message: String?, + exception: Throwable?, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier.padding(16.dp), + ) { + Text( + text = stringResource(R.string.stashapp_errors_header), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleMedium, + ) + message?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleLarge, + ) + } + exception?.localizedMessage?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleMedium, + ) + } + var cause = exception?.cause + while (cause != null) { + cause.localizedMessage?.let { + Text( + text = "Caused by: $it", + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.titleSmall, + ) + } + cause = cause.cause + } + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/TabPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/TabPage.kt index 0b83645e..05045c76 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/TabPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/TabPage.kt @@ -39,6 +39,7 @@ import com.github.damontecres.stashapp.StashApplication import com.github.damontecres.stashapp.api.fragment.StashData import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.StashFindFilter +import com.github.damontecres.stashapp.di.server.ServerPreferences import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.proto.TabType import com.github.damontecres.stashapp.suppliers.FilterArgs @@ -51,7 +52,6 @@ import com.github.damontecres.stashapp.ui.filterArgsSaver import com.github.damontecres.stashapp.ui.tryRequestFocus import com.github.damontecres.stashapp.ui.util.OneTimeLaunchedEffect import com.github.damontecres.stashapp.util.PageFilterKey -import com.github.damontecres.stashapp.util.StashServer import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.milliseconds @@ -203,7 +203,6 @@ data class TabProvider( ) fun createTabFunc( - server: StashServer, itemOnClick: ItemOnClicker, longClicker: LongClicker, composeUiConfig: ComposeUiConfig, @@ -230,7 +229,6 @@ fun createTabFunc( } StashGridTab( name = name, - server = server, initialFilter = filter, itemOnClick = itemOnClick, longClicker = longClicker, @@ -247,7 +245,6 @@ fun createTabFunc( @Composable fun StashGridTab( name: String, - server: StashServer, initialFilter: FilterArgs, itemOnClick: ItemOnClicker, longClicker: LongClicker, @@ -263,13 +260,12 @@ fun StashGridTab( cardContext: ((index: Int, item: StashData) -> CardContext)? = null, ) { val navigationManager = LocalGlobalContext.current.navigationManager - LaunchedEffect(server, initialFilter) { - viewModel.setFilter(server, initialFilter, composeUiConfig.cardSettings.columns) + LaunchedEffect(initialFilter) { + viewModel.setFilter(initialFilter, composeUiConfig.cardSettings.columns) } val pager by viewModel.pager.observeAsState() pager?.let { newPager -> StashGridControls( - server = server, pager = newPager, initialPosition = -1, itemOnClick = itemOnClick, @@ -299,10 +295,9 @@ fun StashGridTab( } fun tabFindFilter( - server: StashServer, + serverPreferences: ServerPreferences, pageFilterKey: PageFilterKey, ): StashFindFilter? = - server.serverPreferences - .getDefaultPageFilter(pageFilterKey) - .findFilter + serverPreferences.defaultPageFilters[pageFilterKey] + ?.findFilter ?.withResolvedRandom() diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt index d9b1916a..faae8d8c 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt @@ -204,8 +204,6 @@ fun DestinationContent( is Destination.Search -> { SearchPage( - server = server, - navigationManager = navManager, uiConfig = composeUiConfig, itemOnClick = itemOnClick, longClicker = longClicker, @@ -215,10 +213,8 @@ fun DestinationContent( is Destination.MarkerDetails -> { MarkerPage( - server = server, uiConfig = composeUiConfig, markerId = destination.markerId, - itemOnClick = itemOnClick, onUpdateTitle = onUpdateTitle, ) } @@ -237,9 +233,7 @@ fun DestinationContent( DataType.PERFORMER -> { PerformerPage( modifier = modifier, - server = server, id = destination.id, - itemOnClick = itemOnClick, longClicker = longClicker, uiConfig = composeUiConfig, onUpdateTitle = onUpdateTitle, @@ -249,10 +243,8 @@ fun DestinationContent( DataType.TAG -> { TagPage( modifier = modifier, - server = server, id = destination.id, includeSubTags = false, - itemOnClick = itemOnClick, longClicker = longClicker, uiConfig = composeUiConfig, onUpdateTitle = onUpdateTitle, @@ -262,10 +254,8 @@ fun DestinationContent( DataType.STUDIO -> { StudioPage( modifier = modifier, - server = server, id = destination.id, includeSubStudios = false, - itemOnClick = itemOnClick, longClicker = longClicker, uiConfig = composeUiConfig, onUpdateTitle = onUpdateTitle, @@ -275,9 +265,7 @@ fun DestinationContent( DataType.GALLERY -> { GalleryPage( modifier = modifier, - server = server, id = destination.id, - itemOnClick = itemOnClick, longClicker = longClicker, uiConfig = composeUiConfig, onUpdateTitle = onUpdateTitle, @@ -287,10 +275,8 @@ fun DestinationContent( DataType.GROUP -> { GroupPage( modifier = modifier, - server = server, id = destination.id, includeSubGroups = false, - itemOnClick = itemOnClick, longClicker = longClicker, uiConfig = composeUiConfig, onUpdateTitle = onUpdateTitle, @@ -299,10 +285,8 @@ fun DestinationContent( DataType.MARKER -> { MarkerPage( - server = server, uiConfig = composeUiConfig, markerId = destination.id, - itemOnClick = itemOnClick, onUpdateTitle = onUpdateTitle, ) } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GalleryPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GalleryPage.kt index 2c77e0f4..ec17c98e 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GalleryPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GalleryPage.kt @@ -1,10 +1,12 @@ package com.github.damontecres.stashapp.ui.pages +import android.app.Application import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -15,6 +17,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.apollographql.apollo.api.Optional import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.api.fragment.GalleryData @@ -25,6 +29,9 @@ 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.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.ItemClicker +import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.proto.TabType import com.github.damontecres.stashapp.suppliers.DataSupplierOverride @@ -33,8 +40,10 @@ import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.LocalGlobalContext import com.github.damontecres.stashapp.ui.components.BasicItemInfo import com.github.damontecres.stashapp.ui.components.EditItem +import com.github.damontecres.stashapp.ui.components.ErrorMessage import com.github.damontecres.stashapp.ui.components.ItemDetails import com.github.damontecres.stashapp.ui.components.ItemOnClicker +import com.github.damontecres.stashapp.ui.components.LoadingPage import com.github.damontecres.stashapp.ui.components.LongClicker import com.github.damontecres.stashapp.ui.components.TabPage import com.github.damontecres.stashapp.ui.components.TabProvider @@ -45,186 +54,256 @@ import com.github.damontecres.stashapp.ui.components.tabFindFilter import com.github.damontecres.stashapp.ui.showAddPerf import com.github.damontecres.stashapp.ui.showAddTag import com.github.damontecres.stashapp.ui.showSetStudio +import com.github.damontecres.stashapp.ui.util.DataLoadingState import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.MutationEngine import com.github.damontecres.stashapp.util.PageFilterKey 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.getUiTabs +import com.github.damontecres.stashapp.util.launchIO import com.github.damontecres.stashapp.util.name import com.github.damontecres.stashapp.util.showSetRatingToast +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel +import org.koin.core.parameter.parametersOf private const val TAG = "GalleryPage" +@KoinViewModel +class GalleryDetailsViewModel( + private val context: Application, + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + val queryEngine: com.github.damontecres.stashapp.di.server.QueryEngine, + val mutationEngine: com.github.damontecres.stashapp.di.server.MutationEngine, + val itemClicker: ItemClicker, + val navigationManager: com.github.damontecres.stashapp.di.services.NavigationManager, + @InjectedParam private val id: String, +) : ViewModel() { + val currentServer get() = serverRepository.currentServer + + private val _state = MutableStateFlow(GalleryState()) + val state: StateFlow = _state + + init { + viewModelScope.launchIO { + _state.update { it.copy(gallery = DataLoadingState.Loading) } + try { + val gallery = queryEngine.getGallery(id) + if (gallery != null) { + val tags = queryEngine.getTags(gallery.tags.map { it.slimTagData.id }) + _state.update { + it.copy( + gallery = DataLoadingState.Success(gallery), + tags = tags, + ) + } + } else { + _state.update { + it.copy( + gallery = DataLoadingState.Error("Not found: $id"), + ) + } + } + } catch (ex: QueryEngine.QueryException) { + Log.e(TAG, "No gallery found with ID $id", ex) + Toast.makeText(context, "No tag found with ID $id", Toast.LENGTH_LONG).show() + } + } + } +} + +data class GalleryState( + val gallery: DataLoadingState = DataLoadingState.Pending, + val tags: List = emptyList(), +) + @Composable fun GalleryPage( - server: StashServer, id: String, - itemOnClick: ItemOnClicker, longClicker: LongClicker, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, + viewModel: GalleryDetailsViewModel = + koinViewModel { + parametersOf(id) + }, ) { val context = LocalContext.current - var gallery by remember { mutableStateOf(null) } - var studio by remember { mutableStateOf(null) } - var tags by remember { mutableStateOf>(listOf()) } - // Remember separately so we don't have refresh the whole page - var rating100 by remember { mutableIntStateOf(0) } - LaunchedEffect(id) { - try { - val queryEngine = QueryEngine(server) - gallery = queryEngine.getGallery(id) - gallery?.let { - rating100 = it.rating100 ?: 0 - studio = it.studio - tags = queryEngine.getTags(it.tags.map { it.slimTagData.id }) - } - } catch (ex: QueryEngine.QueryException) { - Log.e(TAG, "No gallery found with ID $id", ex) - Toast.makeText(context, "No gallery found with ID $id", Toast.LENGTH_LONG).show() - } - } + val state by viewModel.state.collectAsState() + val currentServer by viewModel.currentServer.collectAsState() + val serverPreferences = currentServer.serverPreferences val scope = rememberCoroutineScope() val createTab = createTabFunc( - server, - itemOnClick, + viewModel.itemClicker, longClicker, uiConfig, ) - gallery?.let { gallery -> - val galleries = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(gallery.id)), - modifier = CriterionModifier.INCLUDES_ALL, - ), - ) - val uiTabs = - getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.GALLERY) - val tabs = - listOf( - TabProvider(stringResource(R.string.stashapp_details), TabType.DETAILS) { - GalleryDetails( - modifier = Modifier.fillMaxSize(), - uiConfig = uiConfig, - gallery = gallery, - studio = studio, - tags = tags, - rating100 = rating100, - rating100Click = { newRating100 -> - val mutationEngine = MutationEngine(server) - scope.launch(LoggingCoroutineExceptionHandler(server, scope)) { - val newGallery = - mutationEngine.updateGallery( - galleryId = gallery.id, - rating100 = newRating100, - ) - if (newGallery != null) { - rating100 = newGallery.rating100 ?: 0 - showSetRatingToast( - context, - newGallery.rating100 ?: 0, - server.serverPreferences.ratingsAsStars, - ) - } - } - }, - itemOnClick = itemOnClick, - longClicker = longClicker, - onEdit = { edit -> - val mutationEngine = MutationEngine(server) - val queryEngine = QueryEngine(server) - scope.launch(StashCoroutineExceptionHandler()) { - if (edit.dataType == DataType.TAG) { - val ids = tags.map { it.id }.toMutableList() - edit.action.exec(edit.id, ids) - val newGallery = - mutationEngine.updateGallery( - galleryId = gallery.id, - tagIds = ids, - ) - val newTagIds = - newGallery?.let { it.tags.map { it.slimTagData.id } } - if (newTagIds != null) { - tags = queryEngine.getTags(newTagIds) - if (edit.action == AddRemove.ADD) { - tags - .firstOrNull { it.id == edit.id } - ?.let { showAddTag(it) } - } - } - } else if (edit.dataType == DataType.PERFORMER) { - val ids = - gallery.performers - .map { it.slimPerformerData.id } - .toMutableList() - edit.action.exec(edit.id, ids) + when (val st = state.gallery) { + is DataLoadingState.Error -> { + ErrorMessage(null, st.exception) + } + + DataLoadingState.Loading, + DataLoadingState.Pending, + -> { + LoadingPage(modifier) + } + + is DataLoadingState.Success -> { + val gallery = st.data + var studio by remember { mutableStateOf(gallery.studio) } + var tags by remember { mutableStateOf>(state.tags) } + // Remember separately so we don't have refresh the whole page + var rating100 by remember { mutableIntStateOf(gallery.rating100 ?: 0) } + + val galleries = + remember(gallery) { + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(gallery.id)), + modifier = CriterionModifier.INCLUDES_ALL, + ), + ) + } + val uiTabs = + getUiTabs( + uiConfig.preferences.interfacePreferences.tabPreferences, + DataType.GALLERY, + ) + val tabs = + listOf( + TabProvider(stringResource(R.string.stashapp_details), TabType.DETAILS) { + GalleryDetails( + modifier = Modifier.fillMaxSize(), + uiConfig = uiConfig, + gallery = gallery, + studio = studio, + tags = tags, + rating100 = rating100, + rating100Click = { newRating100 -> + scope.launch( + LoggingCoroutineExceptionHandler( + currentServer, + scope, + ), + ) { val newGallery = - mutationEngine.updateGallery( + viewModel.mutationEngine.updateGallery( galleryId = gallery.id, - performerIds = ids, + rating100 = newRating100, ) if (newGallery != null) { - val perf = queryEngine.getPerformer(edit.id) - if (edit.action == AddRemove.ADD && perf != null) { - showAddPerf(perf) - } - } - } else if (edit.dataType == DataType.STUDIO) { - val newGallery = - mutationEngine.updateGallery( - galleryId = gallery.id, - studioId = edit.id, + rating100 = newGallery.rating100 ?: 0 + showSetRatingToast( + context, + newGallery.rating100 ?: 0, + serverPreferences.ratingsAsStars, ) - if (newGallery != null) { - studio = newGallery.studio - if (edit.action == AddRemove.ADD && newGallery.studio != null) { - showSetStudio(newGallery.studio.name) + } + } + }, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + onEdit = { edit -> + scope.launch(StashCoroutineExceptionHandler()) { + if (edit.dataType == DataType.TAG) { + val ids = tags.map { it.id }.toMutableList() + edit.action.exec(edit.id, ids) + val newGallery = + viewModel.mutationEngine.updateGallery( + galleryId = gallery.id, + tagIds = ids, + ) + val newTagIds = + newGallery?.let { it.tags.map { it.slimTagData.id } } + if (newTagIds != null) { + tags = viewModel.queryEngine.getTags(newTagIds) + if (edit.action == AddRemove.ADD) { + tags + .firstOrNull { it.id == edit.id } + ?.let { showAddTag(it) } + } + } + } else if (edit.dataType == DataType.PERFORMER) { + val ids = + gallery.performers + .map { it.slimPerformerData.id } + .toMutableList() + edit.action.exec(edit.id, ids) + val newGallery = + viewModel.mutationEngine.updateGallery( + galleryId = gallery.id, + performerIds = ids, + ) + if (newGallery != null) { + val perf = viewModel.queryEngine.getPerformer(edit.id) + if (edit.action == AddRemove.ADD && perf != null) { + showAddPerf(perf) + } + } + } else if (edit.dataType == DataType.STUDIO) { + val newGallery = + viewModel.mutationEngine.updateGallery( + galleryId = gallery.id, + studioId = edit.id, + ) + if (newGallery != null) { + studio = newGallery.studio + if (edit.action == AddRemove.ADD && newGallery.studio != null) { + showSetStudio(newGallery.studio.name) + } } } } - } - }, - ) - }, - createTab( - FilterArgs( - dataType = DataType.IMAGE, - findFilter = tabFindFilter(server, PageFilterKey.GALLERY_IMAGES), - objectFilter = ImageFilterType(galleries = galleries), + }, + ) + }, + createTab( + FilterArgs( + dataType = DataType.IMAGE, + findFilter = + tabFindFilter( + serverPreferences, + PageFilterKey.GALLERY_IMAGES, + ), + objectFilter = ImageFilterType(galleries = galleries), + ), ), - ), - createTab( - FilterArgs( - dataType = DataType.SCENE, - objectFilter = SceneFilterType(galleries = galleries), + createTab( + FilterArgs( + dataType = DataType.SCENE, + objectFilter = SceneFilterType(galleries = galleries), + ), ), - ), - createTab( - FilterArgs( - dataType = DataType.PERFORMER, - override = DataSupplierOverride.GalleryPerformer(gallery.id), + createTab( + FilterArgs( + dataType = DataType.PERFORMER, + override = DataSupplierOverride.GalleryPerformer(gallery.id), + ), ), - ), - ).filter { it.type in uiTabs } - val title = AnnotatedString(gallery.name ?: "") - LaunchedEffect(title) { onUpdateTitle?.invoke(title) } - TabPage( - title, - uiConfig.preferences.interfacePreferences.rememberSelectedTab, - tabs, - DataType.GALLERY, - modifier, - onUpdateTitle == null, - ) + ).filter { it.type in uiTabs } + val title = AnnotatedString(gallery.name ?: "") + LaunchedEffect(title) { onUpdateTitle?.invoke(title) } + TabPage( + title, + uiConfig.preferences.interfacePreferences.rememberSelectedTab, + tabs, + DataType.GALLERY, + modifier, + onUpdateTitle == null, + ) + } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GroupPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GroupPage.kt index 0b3bf065..d3ab945c 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GroupPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/GroupPage.kt @@ -1,5 +1,6 @@ package com.github.damontecres.stashapp.ui.pages +import android.app.Application import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.Arrangement @@ -13,6 +14,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -26,6 +28,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade @@ -45,6 +49,9 @@ import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.SortAndDirection import com.github.damontecres.stashapp.data.SortOption import com.github.damontecres.stashapp.data.StashFindFilter +import com.github.damontecres.stashapp.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.ItemClicker +import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.proto.TabType import com.github.damontecres.stashapp.suppliers.DataSupplierOverride @@ -52,9 +59,11 @@ import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.LocalGlobalContext import com.github.damontecres.stashapp.ui.cards.CardContext +import com.github.damontecres.stashapp.ui.components.ErrorMessage import com.github.damontecres.stashapp.ui.components.ItemDetailsFooter import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.ItemsRow +import com.github.damontecres.stashapp.ui.components.LoadingPage import com.github.damontecres.stashapp.ui.components.LongClicker import com.github.damontecres.stashapp.ui.components.Rating100 import com.github.damontecres.stashapp.ui.components.StashGridTab @@ -66,303 +75,365 @@ import com.github.damontecres.stashapp.ui.components.ratingBarHeight import com.github.damontecres.stashapp.ui.components.tabFindFilter import com.github.damontecres.stashapp.ui.filterArgsSaver import com.github.damontecres.stashapp.ui.titleCount +import com.github.damontecres.stashapp.ui.util.DataLoadingState import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.MutationEngine import com.github.damontecres.stashapp.util.PageFilterKey import com.github.damontecres.stashapp.util.QueryEngine -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.getUiTabs import com.github.damontecres.stashapp.util.isNotNullOrBlank +import com.github.damontecres.stashapp.util.launchIO import com.github.damontecres.stashapp.util.showSetRatingToast +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel +import org.koin.core.parameter.parametersOf import kotlin.time.Duration.Companion.seconds private const val TAG = "GroupPage" +@KoinViewModel +class GroupDetailsViewModel( + private val context: Application, + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + val queryEngine: com.github.damontecres.stashapp.di.server.QueryEngine, + val mutationEngine: com.github.damontecres.stashapp.di.server.MutationEngine, + val itemClicker: ItemClicker, + val navigationManager: com.github.damontecres.stashapp.di.services.NavigationManager, + @InjectedParam private val id: String, +) : ViewModel() { + val currentServer get() = serverRepository.currentServer + + private val _state = MutableStateFlow(GroupState()) + val state: StateFlow = _state + + init { + viewModelScope.launchIO { + _state.update { it.copy(group = DataLoadingState.Loading) } + try { + val group = queryEngine.getGroup(id) + if (group != null) { + val tags = queryEngine.getTags(group.tags.map { it.slimTagData.id }) + _state.update { + it.copy( + group = DataLoadingState.Success(group), + tags = tags, + ) + } + } else { + _state.update { + it.copy( + group = DataLoadingState.Error("Not found: $id"), + ) + } + } + } catch (ex: QueryEngine.QueryException) { + Log.e(TAG, "No group found with ID $id", ex) + Toast.makeText(context, "No group found with ID $id", Toast.LENGTH_LONG).show() + } + } + } +} + +data class GroupState( + val group: DataLoadingState = DataLoadingState.Pending, + val tags: List = emptyList(), +) + @Composable fun GroupPage( - server: StashServer, id: String, includeSubGroups: Boolean, - itemOnClick: ItemOnClicker, longClicker: LongClicker, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, + viewModel: GroupDetailsViewModel = + koinViewModel { + parametersOf(id) + }, ) { - var group by remember { mutableStateOf(null) } - // Remember separately so we don't have refresh the whole page - var rating100 by remember { mutableIntStateOf(0) } - var tags by remember { mutableStateOf>(listOf()) } + val state by viewModel.state.collectAsState() + val currentServer by viewModel.currentServer.collectAsState() + val serverPreferences = currentServer.serverPreferences + val context = LocalContext.current - LaunchedEffect(id) { - try { - val queryEngine = QueryEngine(server) - group = queryEngine.getGroup(id) - group?.let { - rating100 = it.rating100 ?: 0 - tags = queryEngine.getTags(it.tags.map { it.slimTagData.id }) - } - } catch (ex: QueryEngine.QueryException) { - Log.e(TAG, "No group found with ID $id", ex) - Toast.makeText(context, "No group found with ID $id", Toast.LENGTH_LONG).show() - } - } val scope = rememberCoroutineScope() + when (val st = state.group) { + is DataLoadingState.Error -> { + ErrorMessage(null, st.exception) + } - group?.let { group -> - val subToggleLabel = - if (group.sub_group_count > 0) stringResource(R.string.stashapp_include_sub_group_content) else null - val groupsFunc = { includeSubGroups: Boolean -> - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(group.id)), - modifier = CriterionModifier.INCLUDES, - depth = Optional.present(if (includeSubGroups) -1 else 0), - ), - ) + DataLoadingState.Loading, + DataLoadingState.Pending, + -> { + LoadingPage(modifier) } - val detailsTab = - TabProvider(stringResource(R.string.stashapp_details), TabType.DETAILS) { - GroupDetails( - modifier = Modifier.fillMaxSize(), - uiConfig = uiConfig, - group = group, - tags = tags, - rating100 = rating100, - rating100Click = { newRating100 -> - val mutationEngine = MutationEngine(server) - scope.launch(LoggingCoroutineExceptionHandler(server, scope)) { - val newGroup = - mutationEngine.updateGroup( - groupId = group.id, - rating100 = newRating100, - ) - if (newGroup != null) { - rating100 = newGroup.rating100 ?: 0 - showSetRatingToast( - context, - newGroup.rating100 ?: 0, - server.serverPreferences.ratingsAsStars, - ) - } - } - }, - itemOnClick = itemOnClick, - longClicker = longClicker, + is DataLoadingState.Success -> { + val group = st.data + // Remember separately so we don't have refresh the whole page + var rating100 by remember { mutableIntStateOf(group.rating100 ?: 0) } + var tags by remember { mutableStateOf>(state.tags) } + val subToggleLabel = + if (group.sub_group_count > 0) stringResource(R.string.stashapp_include_sub_group_content) else null + val groupsFunc = { includeSubGroups: Boolean -> + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(group.id)), + modifier = CriterionModifier.INCLUDES, + depth = Optional.present(if (includeSubGroups) -1 else 0), + ), ) } - // Scenes - var scenesSubTags by rememberSaveable { mutableStateOf(false) } - var scenesFilter by rememberSaveable(scenesSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.SCENE, - findFilter = - tabFindFilter(server, PageFilterKey.GROUP_SCENES) - ?: StashFindFilter( - SortAndDirection( - SortOption.GroupSceneNumber, - SortDirectionEnum.ASC, - ), - ), - objectFilter = SceneFilterType(groups = groupsFunc(scenesSubTags)), - ), - ) - } - val cardContext = - remember(group.id) { CardContext.SceneCardContext(sceneInGroupId = group.id) } - val scenesTab = - remember(scenesFilter, scenesSubTags) { - TabProvider( - context.getString(R.string.stashapp_scenes), - TabType.SCENES, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_scenes), - server = server, - initialFilter = scenesFilter, - itemOnClick = itemOnClick, + val detailsTab = + TabProvider(stringResource(R.string.stashapp_details), TabType.DETAILS) { + GroupDetails( + modifier = Modifier.fillMaxSize(), + uiConfig = uiConfig, + group = group, + tags = tags, + rating100 = rating100, + rating100Click = { newRating100 -> + scope.launch(LoggingCoroutineExceptionHandler(currentServer, scope)) { + val newGroup = + viewModel.mutationEngine.updateGroup( + groupId = group.id, + rating100 = newRating100, + ) + if (newGroup != null) { + rating100 = newGroup.rating100 ?: 0 + showSetRatingToast( + context, + newGroup.rating100 ?: 0, + serverPreferences.ratingsAsStars, + ) + } + } + }, + itemOnClick = viewModel.itemClicker, longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { scenesSubTags = it }, - subToggleChecked = scenesSubTags, - composeUiConfig = uiConfig, - onFilterChange = { scenesFilter = it }, - cardContext = { _, _ -> cardContext }, ) } + + // Scenes + var scenesSubTags by rememberSaveable { mutableStateOf(false) } + var scenesFilter by rememberSaveable(scenesSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.SCENE, + findFilter = + tabFindFilter(serverPreferences, PageFilterKey.GROUP_SCENES) + ?: StashFindFilter( + SortAndDirection( + SortOption.GroupSceneNumber, + SortDirectionEnum.ASC, + ), + ), + objectFilter = SceneFilterType(groups = groupsFunc(scenesSubTags)), + ), + ) } + val cardContext = + remember(group.id) { CardContext.SceneCardContext(sceneInGroupId = group.id) } + val scenesTab = + remember(scenesFilter, scenesSubTags) { + TabProvider( + context.getString(R.string.stashapp_scenes), + TabType.SCENES, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_scenes), + initialFilter = scenesFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { scenesSubTags = it }, + subToggleChecked = scenesSubTags, + composeUiConfig = uiConfig, + onFilterChange = { scenesFilter = it }, + cardContext = { _, _ -> cardContext }, + ) + } + } - // Performers - var performersSubTags by rememberSaveable { mutableStateOf(false) } - var performerFilter by rememberSaveable(performersSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.PERFORMER, - findFilter = tabFindFilter(server, PageFilterKey.GROUP_PERFORMERS), - objectFilter = PerformerFilterType(groups = groupsFunc(performersSubTags)), - ), - ) - } - val performerTab = - remember(performerFilter, performersSubTags) { + // Performers + var performersSubTags by rememberSaveable { mutableStateOf(false) } + var performerFilter by rememberSaveable(performersSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.PERFORMER, + findFilter = + tabFindFilter( + serverPreferences, + PageFilterKey.GROUP_PERFORMERS, + ), + objectFilter = PerformerFilterType(groups = groupsFunc(performersSubTags)), + ), + ) + } + val performerTab = + remember(performerFilter, performersSubTags) { + TabProvider( + context.getString(R.string.stashapp_performers), + TabType.PERFORMERS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_performers), + initialFilter = performerFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { performersSubTags = it }, + subToggleChecked = performersSubTags, + composeUiConfig = uiConfig, + onFilterChange = { performerFilter = it }, + ) + } + } + + // markers + var markersSubTags by rememberSaveable { mutableStateOf(false) } + var markersFilter by rememberSaveable(markersSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.MARKER, + findFilter = null, + objectFilter = + SceneMarkerFilterType( + scene_filter = + Optional.present( + SceneFilterType(groups = groupsFunc(markersSubTags)), + ), + ), + ), + ) + } + val markersTab = + remember(markersSubTags, markersFilter) { + TabProvider( + context.getString(R.string.stashapp_markers), + TabType.MARKERS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_markers), + initialFilter = markersFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { markersSubTags = it }, + subToggleChecked = markersSubTags, + composeUiConfig = uiConfig, + onFilterChange = { markersFilter = it }, + ) + } + } + + // containing groups + val containingGroupsTab = TabProvider( - context.getString(R.string.stashapp_performers), - TabType.PERFORMERS, + stringResource(R.string.stashapp_containing_groups), + TabType.CONTAINING_GROUPS, ) { positionCallback -> + var filter by rememberSaveable(saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + dataType = DataType.GROUP, + override = + DataSupplierOverride.GroupRelationship( + group.id, + GroupRelationshipType.CONTAINING, + ), + ), + ) + } StashGridTab( - name = context.getString(R.string.stashapp_performers), - server = server, - initialFilter = performerFilter, - itemOnClick = itemOnClick, + name = stringResource(R.string.stashapp_containing_groups), + initialFilter = filter, + itemOnClick = viewModel.itemClicker, longClicker = longClicker, modifier = Modifier, positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { performersSubTags = it }, - subToggleChecked = performersSubTags, composeUiConfig = uiConfig, - onFilterChange = { performerFilter = it }, + subToggleLabel = null, + onFilterChange = { filter = it }, ) } - } - // markers - var markersSubTags by rememberSaveable { mutableStateOf(false) } - var markersFilter by rememberSaveable(markersSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.MARKER, - findFilter = null, - objectFilter = - SceneMarkerFilterType( - scene_filter = - Optional.present( - SceneFilterType(groups = groupsFunc(markersSubTags)), - ), - ), - ), - ) - } - val markersTab = - remember(markersSubTags, markersFilter) { + // sub groups + val subGroupsTab = TabProvider( - context.getString(R.string.stashapp_markers), - TabType.MARKERS, + stringResource(R.string.stashapp_sub_groups), + TabType.SUB_GROUPS, ) { positionCallback -> + var filter by rememberSaveable(saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + dataType = DataType.GROUP, + findFilter = + tabFindFilter( + serverPreferences, + PageFilterKey.GROUP_SUB_GROUPS, + ), + override = + DataSupplierOverride.GroupRelationship( + group.id, + GroupRelationshipType.SUB, + ), + ), + ) + } StashGridTab( - name = context.getString(R.string.stashapp_markers), - server = server, - initialFilter = markersFilter, - itemOnClick = itemOnClick, + name = stringResource(R.string.stashapp_sub_groups), + initialFilter = filter, + itemOnClick = viewModel.itemClicker, longClicker = longClicker, + composeUiConfig = uiConfig, modifier = Modifier, positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { markersSubTags = it }, - subToggleChecked = markersSubTags, - composeUiConfig = uiConfig, - onFilterChange = { markersFilter = it }, + subToggleLabel = + if (group.sub_group_count > 0) { + stringResource(R.string.stashapp_include_sub_group_content) + } else { + null + }, + onFilterChange = { filter = it }, ) } - } - // containing groups - val containingGroupsTab = - TabProvider( - stringResource(R.string.stashapp_containing_groups), - TabType.CONTAINING_GROUPS, - ) { positionCallback -> - var filter by rememberSaveable(saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - dataType = DataType.GROUP, - override = - DataSupplierOverride.GroupRelationship( - group.id, - GroupRelationshipType.CONTAINING, - ), - ), - ) - } - StashGridTab( - name = stringResource(R.string.stashapp_containing_groups), - server = server, - initialFilter = filter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - composeUiConfig = uiConfig, - subToggleLabel = null, - onFilterChange = { filter = it }, - ) - } - - // sub groups - val subGroupsTab = - TabProvider( - stringResource(R.string.stashapp_sub_groups), - TabType.SUB_GROUPS, - ) { positionCallback -> - var filter by rememberSaveable(saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - dataType = DataType.GROUP, - findFilter = tabFindFilter(server, PageFilterKey.GROUP_SUB_GROUPS), - override = - DataSupplierOverride.GroupRelationship( - group.id, - GroupRelationshipType.SUB, - ), - ), - ) - } - StashGridTab( - name = stringResource(R.string.stashapp_sub_groups), - server = server, - initialFilter = filter, - itemOnClick = itemOnClick, - longClicker = longClicker, - composeUiConfig = uiConfig, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = - if (group.sub_group_count > 0) { - stringResource(R.string.stashapp_include_sub_group_content) - } else { - null - }, - onFilterChange = { filter = it }, - ) - } - - val uiTabs = - getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.GROUP) - val tabs = - listOf( - detailsTab, - scenesTab, - performerTab, - markersTab, - containingGroupsTab, - subGroupsTab, - ).filter { it.type in uiTabs } - val title = AnnotatedString(group.name) - LaunchedEffect(title) { onUpdateTitle?.invoke(title) } - TabPage( - title, - uiConfig.preferences.interfacePreferences.rememberSelectedTab, - tabs, - DataType.GROUP, - modifier, - onUpdateTitle == null, - ) + val uiTabs = + getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.GROUP) + val tabs = + listOf( + detailsTab, + scenesTab, + performerTab, + markersTab, + containingGroupsTab, + subGroupsTab, + ).filter { it.type in uiTabs } + val title = AnnotatedString(group.name) + LaunchedEffect(title) { onUpdateTitle?.invoke(title) } + TabPage( + title, + uiConfig.preferences.interfacePreferences.rememberSelectedTab, + tabs, + DataType.GROUP, + modifier, + onUpdateTitle == null, + ) + } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerPage.kt index 343fe347..d7d4f66e 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/MarkerPage.kt @@ -43,7 +43,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.material3.ButtonDefaults import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme @@ -69,25 +68,27 @@ import com.github.damontecres.stashapp.ui.components.LongClicker import com.github.damontecres.stashapp.ui.components.TitleValueText import com.github.damontecres.stashapp.ui.components.UpdatedTimestamp import com.github.damontecres.stashapp.ui.tryRequestFocus -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.titleOrFilename import com.github.damontecres.stashapp.views.models.MarkerDetailsViewModel +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @Composable fun MarkerPage( - server: StashServer, markerId: String, - itemOnClick: ItemOnClicker, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, - viewModel: MarkerDetailsViewModel = viewModel(), + viewModel: MarkerDetailsViewModel = + koinViewModel { + parametersOf(markerId) + }, ) { LaunchedEffect(Unit) { - viewModel.init(server, markerId) + viewModel.init() } val marker by viewModel.item.observeAsState() @@ -106,7 +107,7 @@ fun MarkerPage( buildList { add( DialogItem("Go to", Icons.Default.PlayArrow) { - itemOnClick.onClick( + viewModel.itemClicker.onClick( item, filterAndPosition, ) @@ -140,12 +141,11 @@ fun MarkerPage( } onUpdateTitle?.invoke(AnnotatedString(title)) MarkerPageContent( - server = server, marker = marker!!, markerTitle = if (onUpdateTitle == null) title else null, primaryTag = primaryTag!!, tags = tags, - itemOnClick = itemOnClick, + itemOnClick = viewModel.itemClicker::onClick, longClicker = removeLongClicker, uiConfig = uiConfig, setPrimaryTag = viewModel::setPrimaryTag, @@ -167,7 +167,6 @@ fun MarkerPage( @Composable fun MarkerPageContent( - server: StashServer, marker: FullMarkerData, markerTitle: String?, primaryTag: TagData, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PerformerPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PerformerPage.kt index 26962aee..17626c68 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PerformerPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PerformerPage.kt @@ -10,8 +10,8 @@ import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -30,13 +30,7 @@ import androidx.compose.ui.unit.sp import androidx.compose.ui.window.DialogProperties import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.CreationExtras -import androidx.lifecycle.viewmodel.MutableCreationExtras -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner -import androidx.lifecycle.viewmodel.initializer -import androidx.lifecycle.viewmodel.viewModelFactory import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.apollographql.apollo.api.Optional @@ -58,14 +52,16 @@ import com.github.damontecres.stashapp.api.type.SceneMarkerFilterType import com.github.damontecres.stashapp.api.type.StringCriterionInput import com.github.damontecres.stashapp.api.type.StudioFilterType import com.github.damontecres.stashapp.data.DataType +import com.github.damontecres.stashapp.di.server.MutationEngine +import com.github.damontecres.stashapp.di.server.QueryEngine +import com.github.damontecres.stashapp.di.server.ServerPreferences +import com.github.damontecres.stashapp.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.ItemClicker +import com.github.damontecres.stashapp.di.services.ServerLogger 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.proto.StashPreferences import com.github.damontecres.stashapp.proto.TabType import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig -import com.github.damontecres.stashapp.ui.GlobalContext import com.github.damontecres.stashapp.ui.LocalGlobalContext import com.github.damontecres.stashapp.ui.PreviewTheme import com.github.damontecres.stashapp.ui.components.BasicItemInfo @@ -88,28 +84,35 @@ import com.github.damontecres.stashapp.ui.tagPreview import com.github.damontecres.stashapp.ui.titleCount import com.github.damontecres.stashapp.ui.uiConfigPreview import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.MutationEngine import com.github.damontecres.stashapp.util.PageFilterKey -import com.github.damontecres.stashapp.util.QueryEngine -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.ageInYears import com.github.damontecres.stashapp.util.getUiTabs import com.github.damontecres.stashapp.util.isNotNullOrBlank import com.github.damontecres.stashapp.util.showSetRatingToast import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel +import org.koin.core.parameter.parametersOf import kotlin.math.floor import kotlin.math.round import kotlin.math.roundToInt private const val TAG = "PerformerPage" +@KoinViewModel class PerformerDetailsViewModel( - server: StashServer, - val performerId: String, + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + private val queryEngine: QueryEngine, + private val mutationEngine: MutationEngine, + val itemClicker: ItemClicker, + val navigationManager: com.github.damontecres.stashapp.di.services.NavigationManager, + @InjectedParam private val performerId: String, ) : ViewModel() { - private val queryEngine = QueryEngine(server) - private val mutationEngine = MutationEngine(server) - private val exceptionHandler = LoggingCoroutineExceptionHandler(server, viewModelScope) + val currentServer get() = serverRepository.currentServer + private val exceptionHandler = + LoggingCoroutineExceptionHandler(serverRepository.currentServer.value, viewModelScope) private var performer: PerformerData? = null @@ -206,19 +209,6 @@ class PerformerDetailsViewModel( this@PerformerDetailsViewModel.favorite.value = newFavorite } } - - companion object { - val SERVER_KEY = object : CreationExtras.Key {} - val PERFORMER_ID_KEY = object : CreationExtras.Key {} - val Factory: ViewModelProvider.Factory = - viewModelFactory { - initializer { - val server = this[SERVER_KEY]!! - val performerId = this[PERFORMER_ID_KEY]!! - PerformerDetailsViewModel(server, performerId).init() - } - } - } } sealed class PerformerLoadingState { @@ -233,23 +223,16 @@ sealed class PerformerLoadingState { @Composable fun PerformerPage( - server: StashServer, id: String, - itemOnClick: ItemOnClicker, longClicker: LongClicker, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, + viewModel: PerformerDetailsViewModel = + koinViewModel { + parametersOf(id) + }, ) { - val viewModel = - ViewModelProvider.create( - LocalViewModelStoreOwner.current!!, - PerformerDetailsViewModel.Factory, - MutableCreationExtras().apply { - set(PerformerDetailsViewModel.SERVER_KEY, server) - set(PerformerDetailsViewModel.PERFORMER_ID_KEY, id) - }, - )[PerformerDetailsViewModel::class] val loadingState by viewModel.loadingState.observeAsState() val tags by viewModel.tags.observeAsState(listOf()) val studios by viewModel.studios.observeAsState(listOf()) @@ -274,8 +257,9 @@ fun PerformerPage( } is PerformerLoadingState.Success -> { + val currentServer by viewModel.currentServer.collectAsState() PerformerDetailsPage( - server = server, + serverPreferences = currentServer.serverPreferences, perf = state.performer, tags = tags, studios = studios, @@ -284,7 +268,7 @@ fun PerformerPage( onFavoriteClick = viewModel::toggleFavorite, rating100 = rating100, onRatingChange = viewModel::updateRating, - itemOnClick = itemOnClick, + itemOnClick = viewModel.itemClicker, longClicker = longClicker, onUpdateTitle = onUpdateTitle, onEdit = { edit -> @@ -306,7 +290,7 @@ fun PerformerPage( @Composable fun PerformerDetailsPage( - server: StashServer, + serverPreferences: ServerPreferences, perf: PerformerData, tags: List, studios: List, @@ -332,7 +316,7 @@ fun PerformerDetailsPage( ) val uiTabs = getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.PERFORMER) - val createTab = createTabFunc(server, itemOnClick, longClicker, uiConfig) + val createTab = createTabFunc(itemOnClick, longClicker, uiConfig) val tabs = listOf( TabProvider(stringResource(R.string.stashapp_details), TabType.DETAILS) { @@ -355,28 +339,32 @@ fun PerformerDetailsPage( createTab( FilterArgs( dataType = DataType.SCENE, - findFilter = tabFindFilter(server, PageFilterKey.PERFORMER_SCENES), + findFilter = tabFindFilter(serverPreferences, PageFilterKey.PERFORMER_SCENES), objectFilter = SceneFilterType(performers = performers), ), ), createTab( FilterArgs( dataType = DataType.GALLERY, - findFilter = tabFindFilter(server, PageFilterKey.PERFORMER_GALLERIES), + findFilter = + tabFindFilter( + serverPreferences, + PageFilterKey.PERFORMER_GALLERIES, + ), objectFilter = GalleryFilterType(performers = performers), ), ), createTab( FilterArgs( dataType = DataType.IMAGE, - findFilter = tabFindFilter(server, PageFilterKey.PERFORMER_IMAGES), + findFilter = tabFindFilter(serverPreferences, PageFilterKey.PERFORMER_IMAGES), objectFilter = ImageFilterType(performers = performers), ), ), createTab( FilterArgs( dataType = DataType.GROUP, - findFilter = tabFindFilter(server, PageFilterKey.PERFORMER_GROUPS), + findFilter = tabFindFilter(serverPreferences, PageFilterKey.PERFORMER_GROUPS), objectFilter = GroupFilterType(performers = performers), ), ), @@ -395,13 +383,12 @@ fun PerformerDetailsPage( val navigationManager = LocalGlobalContext.current.navigationManager StashGridTab( name = stringResource(R.string.stashapp_appears_with), - server = server, initialFilter = FilterArgs( dataType = DataType.PERFORMER, findFilter = tabFindFilter( - server, + serverPreferences, PageFilterKey.PERFORMER_APPEARS_WITH, ), objectFilter = PerformerFilterType(performers = performers), @@ -712,52 +699,24 @@ private fun PerformerDetailsPreview() { LongClicker { item, filterAndPosition -> } PreviewTheme { - CompositionLocalProvider( - LocalGlobalContext provides - GlobalContext( - StashServer("http://0.0.0.0", null), - object : NavigationManager { - override var previousDestination: Destination? - get() = null - set(value) {} - - override fun navigate(destination: Destination) { - } - - override fun goBack() { - } - - override fun goToMain() { - } - - override fun clearPinFragment() { - } - - override fun addListener(listener: NavigationListener) { - } - }, - StashPreferences.getDefaultInstance(), - ), - ) { - PerformerDetails( - perf = performer, - tags = listOf(tagPreview, tagPreview.copy(id = "723")), - studios = listOf(), - favorite = performer.favorite, - favoriteClick = {}, - rating100 = performer.rating100 ?: 0, - rating100Click = {}, - uiConfig = uiConfigPreview, - itemOnClick = itemOnClick, - longClicker = longClicker, - onShowDialog = {}, - onEdit = {}, - modifier = - Modifier - .fillMaxSize() - .background(color = MaterialTheme.colorScheme.background), - ) - } + PerformerDetails( + perf = performer, + tags = listOf(tagPreview, tagPreview.copy(id = "723")), + studios = listOf(), + favorite = performer.favorite, + favoriteClick = {}, + rating100 = performer.rating100 ?: 0, + rating100Click = {}, + uiConfig = uiConfigPreview, + itemOnClick = itemOnClick, + longClicker = longClicker, + onShowDialog = {}, + onEdit = {}, + modifier = + Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.background), + ) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SearchPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SearchPage.kt index 7a2479db..bbf773a0 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SearchPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/SearchPage.kt @@ -29,7 +29,6 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import com.github.damontecres.stashapp.StashApplication @@ -38,6 +37,10 @@ import com.github.damontecres.stashapp.data.DataType import com.github.damontecres.stashapp.data.SortAndDirection import com.github.damontecres.stashapp.data.SortOption 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.ServerLogger import com.github.damontecres.stashapp.navigation.FilterAndPosition import com.github.damontecres.stashapp.navigation.NavigationManager import com.github.damontecres.stashapp.suppliers.FilterArgs @@ -52,15 +55,21 @@ import com.github.damontecres.stashapp.ui.util.OneTimeLaunchedEffect import com.github.damontecres.stashapp.ui.util.ifElse import com.github.damontecres.stashapp.util.FrontPageParser import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -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.delay import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.annotation.KoinViewModel -class SearchViewModel : ViewModel() { - private lateinit var server: StashServer +@KoinViewModel +class SearchViewModel( + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + private val queryEngine: QueryEngine, + private val mutationEngine: MutationEngine, + val navigationManager: NavigationManager, +) : ViewModel() { private var currentQuery = "" val scenes = MutableLiveData>(listOf()) @@ -85,11 +94,9 @@ class SearchViewModel : ViewModel() { ) fun init( - server: StashServer, initialQuery: String, perPage: Int, ) { - this.server = server search(initialQuery, perPage) } @@ -99,7 +106,6 @@ class SearchViewModel : ViewModel() { ) { if (query.isNotBlank() && query != this.currentQuery) { this.currentQuery = query - val queryEngine = QueryEngine(server) DataType.entries.forEach { val data = mapping[it]!! data.value = listOf() @@ -121,7 +127,7 @@ class SearchViewModel : ViewModel() { viewModelScope.launch( LoggingCoroutineExceptionHandler( - server, + serverRepository.currentServer.value, viewModelScope, toastMessage = "Search for ${ StashApplication.getApplication().getString(it.pluralStringId) @@ -142,14 +148,12 @@ class SearchViewModel : ViewModel() { @Composable fun SearchPage( - server: StashServer, - navigationManager: NavigationManager, uiConfig: ComposeUiConfig, itemOnClick: ItemOnClicker, longClicker: LongClicker, modifier: Modifier = Modifier, initialQuery: String = "", - viewModel: SearchViewModel = viewModel(), + viewModel: SearchViewModel = koinViewModel(), ) { val context = LocalContext.current val scope = rememberCoroutineScope() @@ -180,7 +184,7 @@ fun SearchPage( ) OneTimeLaunchedEffect { - viewModel.init(server, initialQuery, perPage) + viewModel.init(initialQuery, perPage) // focusRequester.tryRequestFocus() } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/StudioPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/StudioPage.kt index b47b8f2a..e13fa541 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/StudioPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/StudioPage.kt @@ -1,10 +1,12 @@ package com.github.damontecres.stashapp.ui.pages +import android.app.Application import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -16,6 +18,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.apollographql.apollo.api.Optional import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.api.fragment.StudioData @@ -31,6 +35,9 @@ 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.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.ItemClicker +import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.navigation.Destination import com.github.damontecres.stashapp.proto.TabType import com.github.damontecres.stashapp.suppliers.FilterArgs @@ -38,8 +45,10 @@ import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.LocalGlobalContext import com.github.damontecres.stashapp.ui.components.BasicItemInfo import com.github.damontecres.stashapp.ui.components.EditItem +import com.github.damontecres.stashapp.ui.components.ErrorMessage import com.github.damontecres.stashapp.ui.components.ItemDetails import com.github.damontecres.stashapp.ui.components.ItemOnClicker +import com.github.damontecres.stashapp.ui.components.LoadingPage import com.github.damontecres.stashapp.ui.components.LongClicker import com.github.damontecres.stashapp.ui.components.StashGridTab import com.github.damontecres.stashapp.ui.components.TabPage @@ -50,432 +59,501 @@ import com.github.damontecres.stashapp.ui.components.tabFindFilter import com.github.damontecres.stashapp.ui.filterArgsSaver import com.github.damontecres.stashapp.ui.showAddTag import com.github.damontecres.stashapp.ui.showSetStudio +import com.github.damontecres.stashapp.ui.util.DataLoadingState import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.MutationEngine import com.github.damontecres.stashapp.util.PageFilterKey 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.getUiTabs +import com.github.damontecres.stashapp.util.launchIO import com.github.damontecres.stashapp.util.showSetRatingToast +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel +import org.koin.core.parameter.parametersOf private const val TAG = "StudioPage" +@KoinViewModel +class StudioDetailsViewModel( + private val context: Application, + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + val queryEngine: com.github.damontecres.stashapp.di.server.QueryEngine, + val mutationEngine: com.github.damontecres.stashapp.di.server.MutationEngine, + val itemClicker: ItemClicker, + val navigationManager: com.github.damontecres.stashapp.di.services.NavigationManager, + @InjectedParam private val id: String, +) : ViewModel() { + val currentServer get() = serverRepository.currentServer + + private val _state = MutableStateFlow(StudioState()) + val state: StateFlow = _state + + init { + viewModelScope.launchIO { + _state.update { it.copy(studio = DataLoadingState.Loading) } + val tagsFunc = { includeSubTags: Boolean -> + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(id)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = Optional.present(if (includeSubTags) -1 else 0), + ), + ) + } + try { + val studio = queryEngine.getStudio(id) + if (studio != null) { + val tags = queryEngine.getTags(studio.tags.map { it.slimTagData.id }) + _state.update { + it.copy( + studio = DataLoadingState.Success(studio), + tags = tags, + ) + } + } else { + _state.update { + it.copy( + studio = DataLoadingState.Error("Not found: $id"), + ) + } + } + } catch (ex: QueryEngine.QueryException) { + Log.e(TAG, "No studio found with ID $id", ex) + Toast.makeText(context, "No tag found with ID $id", Toast.LENGTH_LONG).show() + } + } + } +} + +data class StudioState( + val studio: DataLoadingState = DataLoadingState.Pending, + val tags: List = emptyList(), + val parentStudio: StudioData.Parent_studio? = null, +) + @Composable fun StudioPage( - server: StashServer, id: String, includeSubStudios: Boolean, - itemOnClick: ItemOnClicker, longClicker: LongClicker, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, + viewModel: StudioDetailsViewModel = + koinViewModel { + parametersOf(id) + }, ) { val context = LocalContext.current - var studio by remember { mutableStateOf(null) } - // Remember separately so we don't have refresh the whole page - var favorite by remember { mutableStateOf(false) } - var rating100 by remember { mutableIntStateOf(0) } - var tags by remember { mutableStateOf>(listOf()) } - var parentStudio by remember { mutableStateOf(null) } - LaunchedEffect(id) { - try { - val queryEngine = QueryEngine(server) - studio = queryEngine.getStudio(id) - studio?.let { - favorite = it.favorite - rating100 = it.rating100 ?: 0 - parentStudio = it.parent_studio - tags = queryEngine.getTags(it.tags.map { it.slimTagData.id }) - } - } catch (ex: QueryEngine.QueryException) { - Log.e(TAG, "No studio found with ID $id", ex) - Toast.makeText(context, "No studio found with ID $id", Toast.LENGTH_LONG).show() - } - } - val scope = rememberCoroutineScope() + val state by viewModel.state.collectAsState() + val currentServer by viewModel.currentServer.collectAsState() - studio?.let { studio -> - val subToggleLabel = stringResource(R.string.stashapp_include_sub_studio_content) - val subToggleEnabled = studio.child_studios.isNotEmpty() + when (val st = state.studio) { + is DataLoadingState.Error -> { + ErrorMessage(null, st.exception) + } - val studiosFunc = { includeSubStudios: Boolean -> - Optional.present( - HierarchicalMultiCriterionInput( - value = Optional.present(listOf(studio.id)), - modifier = CriterionModifier.INCLUDES, - depth = Optional.present(if (includeSubStudios) -1 else 0), - ), - ) + DataLoadingState.Loading, + DataLoadingState.Pending, + -> { + LoadingPage(modifier) } - val detailsTab = - TabProvider(stringResource(R.string.stashapp_details), TabType.DETAILS) { - StudioDetails( - studio = studio, - parentStudio = parentStudio, - uiConfig = uiConfig, - favorite = favorite, - favoriteClick = { - val mutationEngine = MutationEngine(server) - scope.launch(LoggingCoroutineExceptionHandler(server, scope)) { - val newStudio = - mutationEngine.updateStudio( - studioId = studio.id, - favorite = !favorite, - ) - if (newStudio != null) { - favorite = newStudio.favorite - if (newStudio.favorite) { - Toast - .makeText( - context, - context.getString(R.string.studio_favorited), - Toast.LENGTH_SHORT, - ).show() - } - } - } - }, - rating100 = rating100, - tags = tags, - rating100Click = { newRating100 -> - val mutationEngine = MutationEngine(server) - scope.launch(LoggingCoroutineExceptionHandler(server, scope)) { - val newStudio = - mutationEngine.updateStudio( - studioId = studio.id, - rating100 = newRating100, - ) - if (newStudio != null) { - rating100 = newStudio.rating100 ?: 0 - showSetRatingToast( - context, - newStudio.rating100 ?: 0, - server.serverPreferences.ratingsAsStars, - ) - } - } - }, - onEdit = { edit -> - val mutationEngine = MutationEngine(server) - val queryEngine = QueryEngine(server) - scope.launch(StashCoroutineExceptionHandler()) { - if (edit.dataType == DataType.TAG) { - val ids = tags.map { it.id }.toMutableList() - edit.action.exec(edit.id, ids) - val newItem = - mutationEngine.updateStudio( + is DataLoadingState.Success -> { + val serverPreferences = currentServer.serverPreferences + val studio = st.data + // Remember separately so we don't have refresh the whole page + var favorite by remember { mutableStateOf(studio.favorite) } + var rating100 by remember { mutableIntStateOf(studio.rating100 ?: 0) } + var tags by remember { mutableStateOf>(state.tags) } + var parentStudio by remember { mutableStateOf(studio.parent_studio) } + val subToggleLabel = stringResource(R.string.stashapp_include_sub_studio_content) + val subToggleEnabled = + remember(studio.child_studios) { studio.child_studios.isNotEmpty() } + + val studiosFunc = { includeSubStudios: Boolean -> + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(studio.id)), + modifier = CriterionModifier.INCLUDES, + depth = Optional.present(if (includeSubStudios) -1 else 0), + ), + ) + } + + val detailsTab = + TabProvider(stringResource(R.string.stashapp_details), TabType.DETAILS) { + StudioDetails( + studio = studio, + parentStudio = parentStudio, + uiConfig = uiConfig, + favorite = favorite, + favoriteClick = { + scope.launch(LoggingCoroutineExceptionHandler(currentServer, scope)) { + val newStudio = + viewModel.mutationEngine.updateStudio( studioId = studio.id, - tagIds = ids, + favorite = !favorite, ) - val newTagIds = - newItem?.let { it.tags.map { it.slimTagData.id } } - if (newTagIds != null) { - tags = queryEngine.getTags(newTagIds) - if (edit.action == AddRemove.ADD) { - tags - .firstOrNull { it.id == edit.id } - ?.let { showAddTag(it) } + if (newStudio != null) { + favorite = newStudio.favorite + if (newStudio.favorite) { + Toast + .makeText( + context, + context.getString(R.string.studio_favorited), + Toast.LENGTH_SHORT, + ).show() } } - } else if (edit.dataType == DataType.STUDIO) { - // TODO the wording in the dialogs & toasts don't specify its the parent studio - val newItem = - mutationEngine.updateStudio( + } + }, + rating100 = rating100, + tags = tags, + rating100Click = { newRating100 -> + scope.launch(LoggingCoroutineExceptionHandler(currentServer, scope)) { + val newStudio = + viewModel.mutationEngine.updateStudio( studioId = studio.id, - parentStudioId = edit.id, + rating100 = newRating100, + ) + if (newStudio != null) { + rating100 = newStudio.rating100 ?: 0 + showSetRatingToast( + context, + newStudio.rating100 ?: 0, + serverPreferences.ratingsAsStars, ) - if (newItem?.parent_studio != null) { - parentStudio = newItem.parent_studio - showSetStudio(newItem.parent_studio.name) } } - } - }, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier.fillMaxSize(), - ) - } - - // Scenes - var scenesSubTags by rememberSaveable { mutableStateOf(false) } - var scenesFilter by rememberSaveable(scenesSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.SCENE, - findFilter = tabFindFilter(server, PageFilterKey.STUDIO_SCENES), - objectFilter = SceneFilterType(studios = studiosFunc(scenesSubTags)), - ), - ) - } - val scenesTab = - remember(scenesFilter, scenesSubTags) { - TabProvider( - context.getString(R.string.stashapp_scenes), - TabType.SCENES, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_scenes), - server = server, - initialFilter = scenesFilter, - itemOnClick = itemOnClick, + }, + onEdit = { edit -> + scope.launch(StashCoroutineExceptionHandler()) { + if (edit.dataType == DataType.TAG) { + val ids = tags.map { it.id }.toMutableList() + edit.action.exec(edit.id, ids) + val newItem = + viewModel.mutationEngine.updateStudio( + studioId = studio.id, + tagIds = ids, + ) + val newTagIds = + newItem?.let { it.tags.map { it.slimTagData.id } } + if (newTagIds != null) { + tags = viewModel.queryEngine.getTags(newTagIds) + if (edit.action == AddRemove.ADD) { + tags + .firstOrNull { it.id == edit.id } + ?.let { showAddTag(it) } + } + } + } else if (edit.dataType == DataType.STUDIO) { + // TODO the wording in the dialogs & toasts don't specify its the parent studio + val newItem = + viewModel.mutationEngine.updateStudio( + studioId = studio.id, + parentStudioId = edit.id, + ) + if (newItem?.parent_studio != null) { + parentStudio = newItem.parent_studio + showSetStudio(newItem.parent_studio.name) + } + } + } + }, + itemOnClick = viewModel.itemClicker, longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { scenesSubTags = it }, - subToggleChecked = scenesSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { scenesFilter = it }, + modifier = Modifier.fillMaxSize(), ) } - } - // Galleries - var galleriesSubTags by rememberSaveable { mutableStateOf(false) } - var galleriesFilter by rememberSaveable(galleriesSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.GALLERY, - findFilter = tabFindFilter(server, PageFilterKey.STUDIO_GALLERIES), - objectFilter = GalleryFilterType(studios = studiosFunc(galleriesSubTags)), - ), - ) - } - val galleriesTab = - remember(galleriesSubTags, galleriesFilter) { - TabProvider( - context.getString(R.string.stashapp_galleries), - TabType.GALLERIES, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_galleries), - server = server, - initialFilter = galleriesFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { galleriesSubTags = it }, - subToggleChecked = galleriesSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { galleriesFilter = it }, - ) + // Scenes + var scenesSubTags by rememberSaveable { mutableStateOf(false) } + var scenesFilter by rememberSaveable(scenesSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.SCENE, + findFilter = tabFindFilter(serverPreferences, PageFilterKey.STUDIO_SCENES), + objectFilter = SceneFilterType(studios = studiosFunc(scenesSubTags)), + ), + ) + } + val scenesTab = + remember(scenesFilter, scenesSubTags) { + TabProvider( + context.getString(R.string.stashapp_scenes), + TabType.SCENES, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_scenes), + initialFilter = scenesFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { scenesSubTags = it }, + subToggleChecked = scenesSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { scenesFilter = it }, + ) + } } + + // Galleries + var galleriesSubTags by rememberSaveable { mutableStateOf(false) } + var galleriesFilter by rememberSaveable(galleriesSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.GALLERY, + findFilter = + tabFindFilter( + serverPreferences, + PageFilterKey.STUDIO_GALLERIES, + ), + objectFilter = GalleryFilterType(studios = studiosFunc(galleriesSubTags)), + ), + ) } - // images - var imagesSubTags by rememberSaveable { mutableStateOf(false) } - var imagesFilter by rememberSaveable(imagesSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.IMAGE, - findFilter = tabFindFilter(server, PageFilterKey.STUDIO_IMAGES), - objectFilter = ImageFilterType(studios = studiosFunc(imagesSubTags)), - ), - ) - } - val imagesTab = - remember(imagesSubTags, imagesFilter) { - TabProvider( - context.getString(R.string.stashapp_images), - TabType.IMAGES, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_images), - server = server, - initialFilter = imagesFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { imagesSubTags = it }, - subToggleChecked = imagesSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { imagesFilter = it }, - ) + val galleriesTab = + remember(galleriesSubTags, galleriesFilter) { + TabProvider( + context.getString(R.string.stashapp_galleries), + TabType.GALLERIES, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_galleries), + initialFilter = galleriesFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { galleriesSubTags = it }, + subToggleChecked = galleriesSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { galleriesFilter = it }, + ) + } } + // images + var imagesSubTags by rememberSaveable { mutableStateOf(false) } + var imagesFilter by rememberSaveable(imagesSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.IMAGE, + findFilter = tabFindFilter(serverPreferences, PageFilterKey.STUDIO_IMAGES), + objectFilter = ImageFilterType(studios = studiosFunc(imagesSubTags)), + ), + ) } - // markers - var markersSubTags by rememberSaveable { mutableStateOf(false) } - var markersFilter by rememberSaveable(markersSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.MARKER, - findFilter = null, - objectFilter = - SceneMarkerFilterType( - scene_filter = - Optional.present( - SceneFilterType(studios = studiosFunc(markersSubTags)), - ), - ), - ), - ) - } - val markersTab = - remember(markersSubTags, markersFilter) { - TabProvider( - context.getString(R.string.stashapp_markers), - TabType.MARKERS, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_markers), - server = server, - initialFilter = markersFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { markersSubTags = it }, - subToggleChecked = markersSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { markersFilter = it }, - ) + val imagesTab = + remember(imagesSubTags, imagesFilter) { + TabProvider( + context.getString(R.string.stashapp_images), + TabType.IMAGES, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_images), + initialFilter = imagesFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { imagesSubTags = it }, + subToggleChecked = imagesSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { imagesFilter = it }, + ) + } } + // markers + var markersSubTags by rememberSaveable { mutableStateOf(false) } + var markersFilter by rememberSaveable(markersSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.MARKER, + findFilter = null, + objectFilter = + SceneMarkerFilterType( + scene_filter = + Optional.present( + SceneFilterType(studios = studiosFunc(markersSubTags)), + ), + ), + ), + ) } + val markersTab = + remember(markersSubTags, markersFilter) { + TabProvider( + context.getString(R.string.stashapp_markers), + TabType.MARKERS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_markers), + initialFilter = markersFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { markersSubTags = it }, + subToggleChecked = markersSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { markersFilter = it }, + ) + } + } - // performers - var performersSubTags by rememberSaveable { mutableStateOf(false) } - var performersFilter by rememberSaveable(performersSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.PERFORMER, - findFilter = tabFindFilter(server, PageFilterKey.STUDIO_PERFORMERS), - objectFilter = PerformerFilterType(studios = studiosFunc(performersSubTags)), - ), - ) - } - val performersTab = - remember(performersSubTags, performersFilter) { - TabProvider( - context.getString(R.string.stashapp_performers), - TabType.PERFORMERS, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_performers), - server = server, - initialFilter = performersFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { performersSubTags = it }, - subToggleChecked = performersSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { performersFilter = it }, - ) + // performers + var performersSubTags by rememberSaveable { mutableStateOf(false) } + var performersFilter by rememberSaveable(performersSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.PERFORMER, + findFilter = + tabFindFilter( + serverPreferences, + PageFilterKey.STUDIO_PERFORMERS, + ), + objectFilter = PerformerFilterType(studios = studiosFunc(performersSubTags)), + ), + ) + } + val performersTab = + remember(performersSubTags, performersFilter) { + TabProvider( + context.getString(R.string.stashapp_performers), + TabType.PERFORMERS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_performers), + initialFilter = performersFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { performersSubTags = it }, + subToggleChecked = performersSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { performersFilter = it }, + ) + } } + + // groups + var groupsSubTags by rememberSaveable { mutableStateOf(false) } + var groupsFilter by rememberSaveable(groupsSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.GROUP, + findFilter = tabFindFilter(serverPreferences, PageFilterKey.STUDIO_GROUPS), + objectFilter = GroupFilterType(studios = studiosFunc(groupsSubTags)), + ), + ) } + val groupsTab = + remember(groupsSubTags, groupsFilter) { + TabProvider( + context.getString(R.string.stashapp_groups), + TabType.GROUPS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_groups), + initialFilter = groupsFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { groupsSubTags = it }, + subToggleChecked = groupsSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { groupsFilter = it }, + ) + } + } - // groups - var groupsSubTags by rememberSaveable { mutableStateOf(false) } - var groupsFilter by rememberSaveable(groupsSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.GROUP, - findFilter = tabFindFilter(server, PageFilterKey.STUDIO_GROUPS), - objectFilter = GroupFilterType(studios = studiosFunc(groupsSubTags)), - ), - ) - } - val groupsTab = - remember(groupsSubTags, groupsFilter) { + var subStudioFilter by rememberSaveable(saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + dataType = DataType.STUDIO, + findFilter = + tabFindFilter( + serverPreferences, + PageFilterKey.STUDIO_CHILDREN, + ), + objectFilter = + StudioFilterType( + parents = + Optional.present( + MultiCriterionInput( + value = Optional.present(listOf(studio.id)), + modifier = CriterionModifier.INCLUDES, + ), + ), + ), + ), + ) + } + val subStudiosTab = TabProvider( - context.getString(R.string.stashapp_groups), - TabType.GROUPS, + stringResource(R.string.stashapp_subsidiary_studios), + TabType.SUBSIDIARY_STUDIOS, ) { positionCallback -> StashGridTab( - name = context.getString(R.string.stashapp_groups), - server = server, - initialFilter = groupsFilter, - itemOnClick = itemOnClick, + name = stringResource(R.string.stashapp_subsidiary_studios), + initialFilter = subStudioFilter, + itemOnClick = viewModel.itemClicker, longClicker = longClicker, modifier = Modifier, positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { groupsSubTags = it }, - subToggleChecked = groupsSubTags, - subToggleEnabled = subToggleEnabled, composeUiConfig = uiConfig, - onFilterChange = { groupsFilter = it }, + subToggleLabel = null, // TODO + onFilterChange = { subStudioFilter = it }, ) } - } - var subStudioFilter by rememberSaveable(saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - dataType = DataType.STUDIO, - findFilter = tabFindFilter(server, PageFilterKey.STUDIO_CHILDREN), - objectFilter = - StudioFilterType( - parents = - Optional.present( - MultiCriterionInput( - value = Optional.present(listOf(studio.id)), - modifier = CriterionModifier.INCLUDES, - ), - ), - ), - ), + val uiTabs = + getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.STUDIO) + val tabs = + listOf( + detailsTab, + scenesTab, + galleriesTab, + imagesTab, + performersTab, + groupsTab, + markersTab, + subStudiosTab, + ).filter { it.type in uiTabs } + val title = AnnotatedString(studio.name) + LaunchedEffect(title) { onUpdateTitle?.invoke(title) } + TabPage( + title, + uiConfig.preferences.interfacePreferences.rememberSelectedTab, + tabs, + DataType.STUDIO, + modifier, + onUpdateTitle == null, ) } - val subStudiosTab = - TabProvider( - stringResource(R.string.stashapp_subsidiary_studios), - TabType.SUBSIDIARY_STUDIOS, - ) { positionCallback -> - StashGridTab( - name = stringResource(R.string.stashapp_subsidiary_studios), - server = server, - initialFilter = subStudioFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - composeUiConfig = uiConfig, - subToggleLabel = null, // TODO - onFilterChange = { subStudioFilter = it }, - ) - } - - val uiTabs = - getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.STUDIO) - val tabs = - listOf( - detailsTab, - scenesTab, - galleriesTab, - imagesTab, - performersTab, - groupsTab, - markersTab, - subStudiosTab, - ).filter { it.type in uiTabs } - val title = AnnotatedString(studio.name) - LaunchedEffect(title) { onUpdateTitle?.invoke(title) } - TabPage( - title, - uiConfig.preferences.interfacePreferences.rememberSelectedTab, - tabs, - DataType.STUDIO, - modifier, - onUpdateTitle == null, - ) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/TagPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/TagPage.kt index 4481c983..8e5903ab 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/TagPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/TagPage.kt @@ -1,11 +1,13 @@ package com.github.damontecres.stashapp.ui.pages +import android.app.Application import android.util.Log import android.widget.Toast import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -17,6 +19,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.apollographql.apollo.api.Optional import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.api.fragment.TagData @@ -30,13 +34,18 @@ import com.github.damontecres.stashapp.api.type.SceneMarkerFilterType 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.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.ItemClicker +import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.proto.TabType import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig import com.github.damontecres.stashapp.ui.components.BasicItemInfo +import com.github.damontecres.stashapp.ui.components.ErrorMessage import com.github.damontecres.stashapp.ui.components.ItemDetails import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.ItemsRow +import com.github.damontecres.stashapp.ui.components.LoadingPage import com.github.damontecres.stashapp.ui.components.LongClicker import com.github.damontecres.stashapp.ui.components.StashGridTab import com.github.damontecres.stashapp.ui.components.TabPage @@ -44,33 +53,103 @@ import com.github.damontecres.stashapp.ui.components.TabProvider import com.github.damontecres.stashapp.ui.components.TableRow import com.github.damontecres.stashapp.ui.components.tabFindFilter import com.github.damontecres.stashapp.ui.filterArgsSaver +import com.github.damontecres.stashapp.ui.util.DataLoadingState import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.MutationEngine import com.github.damontecres.stashapp.util.PageFilterKey import com.github.damontecres.stashapp.util.QueryEngine -import com.github.damontecres.stashapp.util.StashServer import com.github.damontecres.stashapp.util.getUiTabs +import com.github.damontecres.stashapp.util.launchIO +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.KoinViewModel +import org.koin.core.parameter.parametersOf private const val TAG = "TagPage" +@KoinViewModel +class TagDetailsViewModel( + private val context: Application, + private val serverRepository: ServerRepository, + private val serverLogger: ServerLogger, + private val queryEngine: com.github.damontecres.stashapp.di.server.QueryEngine, + val mutationEngine: com.github.damontecres.stashapp.di.server.MutationEngine, + val itemClicker: ItemClicker, + val navigationManager: com.github.damontecres.stashapp.di.services.NavigationManager, + @InjectedParam private val id: String, +) : ViewModel() { + val currentServer get() = serverRepository.currentServer + + private val _state = MutableStateFlow(TagState()) + val state: StateFlow = _state + + init { + viewModelScope.launchIO { + _state.update { it.copy(tag = DataLoadingState.Loading) } + val tagsFunc = { includeSubTags: Boolean -> + Optional.present( + HierarchicalMultiCriterionInput( + value = Optional.present(listOf(id)), + modifier = CriterionModifier.INCLUDES_ALL, + depth = Optional.present(if (includeSubTags) -1 else 0), + ), + ) + } + try { + val tag = queryEngine.getTag(id) + if (tag != null) { + val childTags = + queryEngine.findTags(tagFilter = TagFilterType(parents = tagsFunc(false))) + val parentTags = + queryEngine.findTags(tagFilter = TagFilterType(children = tagsFunc(false))) + _state.update { + it.copy( + tag = DataLoadingState.Success(tag), + childTags = childTags, + parentTags = parentTags, + ) + } + } else { + _state.update { + it.copy( + tag = DataLoadingState.Error("Not found: $id"), + ) + } + } + } catch (ex: QueryEngine.QueryException) { + Log.e(TAG, "No tag found with ID $id", ex) + Toast.makeText(context, "No tag found with ID $id", Toast.LENGTH_LONG).show() + } + } + } +} + +data class TagState( + val tag: DataLoadingState = DataLoadingState.Pending, + val parentTags: List = emptyList(), + val childTags: List = emptyList(), +) + @Composable fun TagPage( - server: StashServer, id: String, includeSubTags: Boolean, - itemOnClick: ItemOnClicker, longClicker: LongClicker, uiConfig: ComposeUiConfig, modifier: Modifier = Modifier, onUpdateTitle: ((AnnotatedString) -> Unit)? = null, + viewModel: TagDetailsViewModel = + koinViewModel { + parametersOf(id) + }, ) { val context = LocalContext.current - var tag by remember { mutableStateOf(null) } - // Remember separately so we don't have refresh the whole page - var favorite by remember { mutableStateOf(false) } - var parentTags by remember { mutableStateOf>(listOf()) } - var childTags by remember { mutableStateOf>(listOf()) } + val state by viewModel.state.collectAsState() + val currentServer by viewModel.currentServer.collectAsState() + val serverPreferences = currentServer.serverPreferences val tagsFunc = { includeSubTags: Boolean -> Optional.present( @@ -82,298 +161,298 @@ fun TagPage( ) } - LaunchedEffect(id) { - try { - val queryEngine = QueryEngine(server) - tag = queryEngine.getTag(id) - tag?.let { - favorite = it.favorite - } - childTags = queryEngine.findTags(tagFilter = TagFilterType(parents = tagsFunc(false))) - parentTags = queryEngine.findTags(tagFilter = TagFilterType(children = tagsFunc(false))) - } catch (ex: QueryEngine.QueryException) { - Log.e(TAG, "No tag found with ID $id", ex) - Toast.makeText(context, "No tag found with ID $id", Toast.LENGTH_LONG).show() + when (val st = state.tag) { + is DataLoadingState.Error -> { + ErrorMessage(null, st.exception) } - } - val scope = rememberCoroutineScope() + DataLoadingState.Loading, + DataLoadingState.Pending, + -> { + LoadingPage(modifier) + } - tag?.let { tag -> - val subToggleLabel = stringResource(R.string.stashapp_include_sub_tag_content) - val subToggleEnabled = tag.child_count > 0 + is DataLoadingState.Success -> { + val tag = st.data + var favorite by remember { mutableStateOf(tag.favorite) } + val scope = rememberCoroutineScope() + val subToggleLabel = stringResource(R.string.stashapp_include_sub_tag_content) + val subToggleEnabled = tag.child_count > 0 - val uiTabs = - getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.TAG) + val uiTabs = + getUiTabs(uiConfig.preferences.interfacePreferences.tabPreferences, DataType.TAG) - val detailsTab = - remember { - TabProvider( - context.getString(R.string.stashapp_details), - TabType.DETAILS, - ) { - TagDetails( - modifier = Modifier.fillMaxSize(), - uiConfig = uiConfig, - tag = tag, - parentTags = parentTags, - childTags = childTags, - favorite = favorite, - favoriteClick = { - val mutationEngine = MutationEngine(server) - scope.launch(LoggingCoroutineExceptionHandler(server, scope)) { - val newTag = - mutationEngine.setTagFavorite( - tagId = tag.id, - favorite = !favorite, - ) - if (newTag != null) { - favorite = newTag.favorite - if (newTag.favorite) { - Toast - .makeText( - context, - "Tag favorited!", - Toast.LENGTH_SHORT, - ).show() + val detailsTab = + remember { + TabProvider( + context.getString(R.string.stashapp_details), + TabType.DETAILS, + ) { + TagDetails( + modifier = Modifier.fillMaxSize(), + uiConfig = uiConfig, + tag = tag, + parentTags = state.parentTags, + childTags = state.childTags, + favorite = favorite, + favoriteClick = { + scope.launch( + LoggingCoroutineExceptionHandler( + currentServer, + scope, + ), + ) { + val newTag = + viewModel.mutationEngine.setTagFavorite( + tagId = tag.id, + favorite = !favorite, + ) + if (newTag != null) { + favorite = newTag.favorite + if (newTag.favorite) { + Toast + .makeText( + context, + "Tag favorited!", + Toast.LENGTH_SHORT, + ).show() + } } } - } - }, - itemOnClick = itemOnClick, - longClicker = longClicker, - ) + }, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + ) + } } - } - // Scenes - var scenesSubTags by rememberSaveable { mutableStateOf(false) } - var scenesFilter by rememberSaveable(scenesSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.SCENE, - findFilter = tabFindFilter(server, PageFilterKey.TAG_SCENES), - objectFilter = SceneFilterType(tags = tagsFunc(scenesSubTags)), - ), - ) - } - val scenesTab = - remember(scenesFilter, scenesSubTags) { - TabProvider( - context.getString(R.string.stashapp_scenes), - TabType.SCENES, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_scenes), - server = server, - initialFilter = scenesFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { scenesSubTags = it }, - subToggleChecked = scenesSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { scenesFilter = it }, - ) - } + // Scenes + var scenesSubTags by rememberSaveable { mutableStateOf(false) } + var scenesFilter by rememberSaveable(scenesSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.SCENE, + findFilter = + tabFindFilter( + currentServer.serverPreferences, + PageFilterKey.TAG_SCENES, + ), + objectFilter = SceneFilterType(tags = tagsFunc(scenesSubTags)), + ), + ) } - - // Galleries - var galleriesSubTags by rememberSaveable { mutableStateOf(false) } - var galleriesFilter by rememberSaveable(galleriesSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.GALLERY, - findFilter = tabFindFilter(server, PageFilterKey.TAG_GALLERIES), - objectFilter = GalleryFilterType(tags = tagsFunc(galleriesSubTags)), - ), - ) - } - val galleriesTab = - remember(galleriesSubTags, galleriesFilter) { - TabProvider( - context.getString(R.string.stashapp_galleries), - TabType.GALLERIES, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_galleries), - server = server, - initialFilter = galleriesFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { galleriesSubTags = it }, - subToggleChecked = galleriesSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { galleriesFilter = it }, - ) + val scenesTab = + remember(scenesFilter, scenesSubTags) { + TabProvider( + context.getString(R.string.stashapp_scenes), + TabType.SCENES, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_scenes), + initialFilter = scenesFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { scenesSubTags = it }, + subToggleChecked = scenesSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { scenesFilter = it }, + ) + } } + + // Galleries + var galleriesSubTags by rememberSaveable { mutableStateOf(false) } + var galleriesFilter by rememberSaveable(galleriesSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.GALLERY, + findFilter = tabFindFilter(serverPreferences, PageFilterKey.TAG_GALLERIES), + objectFilter = GalleryFilterType(tags = tagsFunc(galleriesSubTags)), + ), + ) } - // images - var imagesSubTags by rememberSaveable { mutableStateOf(false) } - var imagesFilter by rememberSaveable(imagesSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.IMAGE, - findFilter = tabFindFilter(server, PageFilterKey.TAG_IMAGES), - objectFilter = ImageFilterType(tags = tagsFunc(imagesSubTags)), - ), - ) - } - val imagesTab = - remember(imagesSubTags, imagesFilter) { - TabProvider( - context.getString(R.string.stashapp_images), - TabType.IMAGES, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_images), - server = server, - initialFilter = imagesFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { imagesSubTags = it }, - subToggleChecked = imagesSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { imagesFilter = it }, - ) + val galleriesTab = + remember(galleriesSubTags, galleriesFilter) { + TabProvider( + context.getString(R.string.stashapp_galleries), + TabType.GALLERIES, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_galleries), + initialFilter = galleriesFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { galleriesSubTags = it }, + subToggleChecked = galleriesSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { galleriesFilter = it }, + ) + } } + // images + var imagesSubTags by rememberSaveable { mutableStateOf(false) } + var imagesFilter by rememberSaveable(imagesSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.IMAGE, + findFilter = tabFindFilter(serverPreferences, PageFilterKey.TAG_IMAGES), + objectFilter = ImageFilterType(tags = tagsFunc(imagesSubTags)), + ), + ) } - // markers - var markersSubTags by rememberSaveable { mutableStateOf(false) } - var markersFilter by rememberSaveable(markersSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.MARKER, - findFilter = tabFindFilter(server, PageFilterKey.TAG_MARKERS), - objectFilter = SceneMarkerFilterType(tags = tagsFunc(markersSubTags)), - ), - ) - } - val markersTab = - remember(markersSubTags, markersFilter) { - TabProvider( - context.getString(R.string.stashapp_markers), - TabType.MARKERS, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_markers), - server = server, - initialFilter = markersFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { markersSubTags = it }, - subToggleChecked = markersSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { markersFilter = it }, - ) + val imagesTab = + remember(imagesSubTags, imagesFilter) { + TabProvider( + context.getString(R.string.stashapp_images), + TabType.IMAGES, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_images), + initialFilter = imagesFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { imagesSubTags = it }, + subToggleChecked = imagesSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { imagesFilter = it }, + ) + } } + // markers + var markersSubTags by rememberSaveable { mutableStateOf(false) } + var markersFilter by rememberSaveable(markersSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.MARKER, + findFilter = tabFindFilter(serverPreferences, PageFilterKey.TAG_MARKERS), + objectFilter = SceneMarkerFilterType(tags = tagsFunc(markersSubTags)), + ), + ) } + val markersTab = + remember(markersSubTags, markersFilter) { + TabProvider( + context.getString(R.string.stashapp_markers), + TabType.MARKERS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_markers), + initialFilter = markersFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { markersSubTags = it }, + subToggleChecked = markersSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { markersFilter = it }, + ) + } + } - // performers - var performersSubTags by rememberSaveable { mutableStateOf(false) } - var performersFilter by rememberSaveable(performersSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.PERFORMER, - findFilter = tabFindFilter(server, PageFilterKey.TAG_PERFORMERS), - objectFilter = PerformerFilterType(tags = tagsFunc(performersSubTags)), - ), - ) - } - val performersTab = - remember(performersSubTags, performersFilter) { - TabProvider( - context.getString(R.string.stashapp_performers), - TabType.PERFORMERS, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_performers), - server = server, - initialFilter = performersFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { performersSubTags = it }, - subToggleChecked = performersSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { performersFilter = it }, - ) + // performers + var performersSubTags by rememberSaveable { mutableStateOf(false) } + var performersFilter by rememberSaveable(performersSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.PERFORMER, + findFilter = tabFindFilter(serverPreferences, PageFilterKey.TAG_PERFORMERS), + objectFilter = PerformerFilterType(tags = tagsFunc(performersSubTags)), + ), + ) + } + val performersTab = + remember(performersSubTags, performersFilter) { + TabProvider( + context.getString(R.string.stashapp_performers), + TabType.PERFORMERS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_performers), + initialFilter = performersFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { performersSubTags = it }, + subToggleChecked = performersSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { performersFilter = it }, + ) + } } + + // studios + var studiosSubTags by rememberSaveable { mutableStateOf(false) } + var studiosFilter by rememberSaveable(studiosSubTags, saver = filterArgsSaver) { + mutableStateOf( + FilterArgs( + DataType.STUDIO, + findFilter = null, + objectFilter = StudioFilterType(tags = tagsFunc(studiosSubTags)), + ), + ) } + val studiosTab = + remember(studiosSubTags, studiosFilter) { + TabProvider( + context.getString(R.string.stashapp_studios), + TabType.STUDIOS, + ) { positionCallback -> + StashGridTab( + name = context.getString(R.string.stashapp_studios), + initialFilter = studiosFilter, + itemOnClick = viewModel.itemClicker, + longClicker = longClicker, + modifier = Modifier, + positionCallback = positionCallback, + subToggleLabel = subToggleLabel, + onSubToggleCheck = { studiosSubTags = it }, + subToggleChecked = studiosSubTags, + subToggleEnabled = subToggleEnabled, + composeUiConfig = uiConfig, + onFilterChange = { studiosFilter = it }, + ) + } + } - // studios - var studiosSubTags by rememberSaveable { mutableStateOf(false) } - var studiosFilter by rememberSaveable(studiosSubTags, saver = filterArgsSaver) { - mutableStateOf( - FilterArgs( - DataType.STUDIO, - findFilter = null, - objectFilter = StudioFilterType(tags = tagsFunc(studiosSubTags)), - ), + val tabs = + listOf( + detailsTab, + scenesTab, + galleriesTab, + imagesTab, + markersTab, + performersTab, + studiosTab, + ).filter { it.type in uiTabs } + val title = AnnotatedString(tag.name) + LaunchedEffect(title) { onUpdateTitle?.invoke(title) } + TabPage( + title, + uiConfig.preferences.interfacePreferences.rememberSelectedTab, + tabs, + DataType.TAG, + modifier, + onUpdateTitle == null, ) } - val studiosTab = - remember(studiosSubTags, studiosFilter) { - TabProvider( - context.getString(R.string.stashapp_studios), - TabType.STUDIOS, - ) { positionCallback -> - StashGridTab( - name = context.getString(R.string.stashapp_studios), - server = server, - initialFilter = studiosFilter, - itemOnClick = itemOnClick, - longClicker = longClicker, - modifier = Modifier, - positionCallback = positionCallback, - subToggleLabel = subToggleLabel, - onSubToggleCheck = { studiosSubTags = it }, - subToggleChecked = studiosSubTags, - subToggleEnabled = subToggleEnabled, - composeUiConfig = uiConfig, - onFilterChange = { studiosFilter = it }, - ) - } - } - - val tabs = - listOf( - detailsTab, - scenesTab, - galleriesTab, - imagesTab, - markersTab, - performersTab, - studiosTab, - ).filter { it.type in uiTabs } - val title = AnnotatedString(tag.name) - LaunchedEffect(title) { onUpdateTitle?.invoke(title) } - TabPage( - title, - uiConfig.preferences.interfacePreferences.rememberSelectedTab, - tabs, - DataType.TAG, - modifier, - onUpdateTitle == null, - ) } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt index dc7b03cf..e075790f 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/views/models/MarkerDetailsViewModel.kt @@ -11,6 +11,7 @@ import com.github.damontecres.stashapp.api.type.SceneMarkerUpdateInput 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.ItemClicker import com.github.damontecres.stashapp.di.services.NavigationManager import com.github.damontecres.stashapp.di.services.ServerLogger import com.github.damontecres.stashapp.ui.showAddTag @@ -27,6 +28,7 @@ class MarkerDetailsViewModel( private val serverRepository: ServerRepository, private val serverLogger: ServerLogger, private val queryEngine: QueryEngine, + val itemClicker: ItemClicker, val mutationEngine: MutationEngine, val navigationManager: NavigationManager, @InjectedParam private val id: String, From e7165ee1a0cfa0fdd9bd2b99cd8569fda69d2b58 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 18:36:50 -0400 Subject: [PATCH 07/25] Checkpoint player --- .../damontecres/stashapp/MainActivity.kt | 1 - .../stashapp/di/services/PlayerFactory.kt | 140 +++++++++++++++ .../navigation/NavigationManagerCompose.kt | 1 - .../stashapp/ui/FilterViewModel.kt | 7 + .../stashapp/ui/NavDrawerFragment.kt | 167 ------------------ .../playback/PlaybackPageContent.kt | 20 +-- .../stashapp/ui/nav/ApplicationContent.kt | 6 +- .../stashapp/ui/nav/DestinationContent.kt | 20 ++- .../damontecres/stashapp/ui/nav/NavDrawer.kt | 5 +- .../stashapp/ui/nav/NavScaffold.kt | 5 +- .../stashapp/ui/pages/PlaybackPage.kt | 42 +++-- .../ui/pages/PlaybackPageViewModel.kt | 5 + 12 files changed, 212 insertions(+), 207 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/stashapp/di/services/PlayerFactory.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt diff --git a/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt b/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt index 9f7009de..762bfa37 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/MainActivity.kt @@ -22,7 +22,6 @@ 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.ui.AppTheme -import com.github.damontecres.stashapp.ui.NavDrawerFragment.Companion.TAG import com.github.damontecres.stashapp.ui.chooseColorScheme import com.github.damontecres.stashapp.ui.defaultColorSchemeSet import com.github.damontecres.stashapp.ui.nav.ApplicationContent 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 00000000..83195e3b --- /dev/null +++ b/app/src/main/java/com/github/damontecres/stashapp/di/services/PlayerFactory.kt @@ -0,0 +1,140 @@ +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.StashExoPlayer.Companion.getInstance +import com.github.damontecres.stashapp.di.AuthHttpClient +import com.github.damontecres.stashapp.di.server.ServerRepository +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 + + /** + * 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 + } +} diff --git a/app/src/main/java/com/github/damontecres/stashapp/navigation/NavigationManagerCompose.kt b/app/src/main/java/com/github/damontecres/stashapp/navigation/NavigationManagerCompose.kt index a0e37186..c3949fae 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/navigation/NavigationManagerCompose.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/navigation/NavigationManagerCompose.kt @@ -11,7 +11,6 @@ import com.github.damontecres.stashapp.R import com.github.damontecres.stashapp.RootActivity import com.github.damontecres.stashapp.UpdateChangelogFragment import com.github.damontecres.stashapp.setup.readonly.SettingsPinEntryFragment -import com.github.damontecres.stashapp.ui.NavDrawerFragment import com.github.damontecres.stashapp.util.putDestination import com.github.damontecres.stashapp.views.models.ServerViewModel import dev.olshevski.navigation.reimagined.NavController diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterViewModel.kt index 944e444d..bd171061 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/FilterViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/FilterViewModel.kt @@ -1,6 +1,7 @@ package com.github.damontecres.stashapp.ui import android.util.Log +import androidx.datastore.core.DataStore import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -11,6 +12,8 @@ import com.github.damontecres.stashapp.data.DataType 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.PlayerFactory +import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.suppliers.DataSupplierFactory import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.suppliers.StashPagingSource @@ -27,6 +30,10 @@ class FilterViewModel( private val serverRepository: ServerRepository, private val queryEngine: QueryEngine, val navigationManager: NavigationManager, + // TODO remove this + val preferences: DataStore, + // TODO remove this + val playerFactory: PlayerFactory, ) : ViewModel() { val pager = MutableLiveData>() diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt deleted file mode 100644 index 79118b61..00000000 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/NavDrawerFragment.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.github.damontecres.stashapp.ui - -import android.os.Bundle -import android.util.Log -import android.view.View -import android.widget.Toast -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.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.Fragment -import androidx.fragment.app.activityViewModels -import androidx.tv.material3.MaterialTheme -import coil3.annotation.ExperimentalCoilApi -import com.github.damontecres.stashapp.R -import com.github.damontecres.stashapp.navigation.Destination -import com.github.damontecres.stashapp.navigation.NavigationManagerCompose -import com.github.damontecres.stashapp.ui.components.server.InitialSetup -import com.github.damontecres.stashapp.ui.components.server.ManageServers -import com.github.damontecres.stashapp.ui.nav.ApplicationContent -import com.github.damontecres.stashapp.util.preferences -import com.github.damontecres.stashapp.views.models.ServerViewModel -import dev.olshevski.navigation.reimagined.NavBackHandler -import dev.olshevski.navigation.reimagined.NavController -import dev.olshevski.navigation.reimagined.navigate -import dev.olshevski.navigation.reimagined.popUpTo -import dev.olshevski.navigation.reimagined.rememberNavController -import kotlin.time.ExperimentalTime - -/** - * Main fragment for the compose UI - */ -class NavDrawerFragment : Fragment(R.layout.compose_frame) { - private val serverViewModel: ServerViewModel by activityViewModels() - - var navController: NavController? = null - - @OptIn(ExperimentalCoilApi::class, ExperimentalTime::class) - override fun onViewCreated( - view: View, - savedInstanceState: Bundle?, - ) { - super.onViewCreated(view, savedInstanceState) - val composeView = view.findViewById(R.id.compose_view) - composeView.apply { - // Dispose of the Composition when the view's LifecycleOwner is destroyed - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - setContent { - val server by serverViewModel.currentServer.observeAsState() - val preferences by context.preferences.data.collectAsState(null) - val isSystemInDarkTheme = isSystemInDarkTheme() - preferences?.let { preferences -> -// CoilConfig(serverViewModel, preferences) - var colorScheme by - remember { - mutableStateOf( - getTheme( - requireContext(), - preferences.interfacePreferences.themeStyle, - preferences.interfacePreferences.theme, - isSystemInDarkTheme, - ), - ) - } - AppTheme(colorScheme = colorScheme) { - key(server) { - val navController = rememberNavController(Destination.Main) - this@NavDrawerFragment.navController = navController - NavBackHandler(navController) - val navManager = - (serverViewModel.navigationManager as NavigationManagerCompose) -// navManager.controller = navController - - val navCommand by serverViewModel.command.observeAsState() - LaunchedEffect(navCommand) { - navCommand?.let { cmd -> - Log.v(TAG, "cmd=$cmd, server=$server") - if (cmd.popUpToMain) { - navController.popUpTo { it == Destination.Main } - } - navController.navigate(cmd.destination) - } - } - if (server == null && serverViewModel.destination.value is Destination.Setup) { - InitialSetup( - modifier = Modifier.fillMaxSize(), - serverViewModel = serverViewModel, - ) - } else if (server == null && - (navCommand?.destination is Destination.ManageServers || navCommand?.destination is Destination.Main) - ) { - ManageServers( - currentServer = null, - onSwitchServer = serverViewModel::switchServer, - modifier = Modifier.fillMaxSize(), - ) - } else { - server?.let { currentServer -> - CompositionLocalProvider( - LocalGlobalContext provides - GlobalContext( - currentServer, - navManager, - preferences, - ), - ) { - ApplicationContent( - server = currentServer, - preferences = preferences, - navigationManager = navManager, - navController = navController, - onSwitchServer = { serverViewModel.switchServer(it) }, - onChangeTheme = { name -> - try { - colorScheme = - chooseColorScheme( - preferences.interfacePreferences.themeStyle, - isSystemInDarkTheme, - if (name.isNullOrBlank() || name == "default") { - defaultColorSchemeSet - } else { - readThemeJson( - requireContext(), - name, - ) - }, - ) - Log.i(TAG, "Updated theme") - } catch (ex: Exception) { - Log.e(TAG, "Exception changing theme", ex) - Toast - .makeText( - requireContext(), - "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 - ) - } - } - } - } - } - } - } - } - } - - companion object { - const val TAG = "NavDrawerFragment" - } -} diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt index 15f84983..3835c605 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/components/playback/PlaybackPageContent.kt @@ -82,7 +82,6 @@ import coil3.request.CachePolicy import coil3.request.ImageRequest import coil3.size.Scale import com.github.damontecres.stashapp.StashApplication -import com.github.damontecres.stashapp.StashExoPlayer import com.github.damontecres.stashapp.api.fragment.FullSceneData import com.github.damontecres.stashapp.api.fragment.PerformerData import com.github.damontecres.stashapp.api.fragment.StashData @@ -203,7 +202,7 @@ class PlaybackViewModel( addCloseable("tracking") { trackActivityListener?.let { it.release(player.currentPosition) - StashExoPlayer.removeListener(it) + player.removeListener(it) } } } @@ -227,20 +226,20 @@ class PlaybackViewModel( ) trackActivityListener?.apply { release() - StashExoPlayer.removeListener(this) + player.removeListener(this) } tag.item.let { trackActivityListener = TrackActivityPlaybackListener( mutationEngine = mutationEngine, - minimumPlayPercent = TODO(), + minimumPlayPercent = 0f, // TODO scene = it, getCurrentPosition = { player.currentPosition }, ) } - trackActivityListener?.let { StashExoPlayer.addListener(it) } + trackActivityListener?.let { player.addListener(it) } } refreshScene(tag.item.id) @@ -636,7 +635,7 @@ fun PlaybackPageContent( onStopOrDispose { savedStartPosition = player.currentPosition currentPlaylistIndex = player.currentMediaItemIndex - StashExoPlayer.releasePlayer() + player.release() } } @@ -711,13 +710,13 @@ fun PlaybackPageContent( controllerViewState.showControls() } } - StashExoPlayer.addListener( + (player as? ExoPlayer)?.addAnalyticsListener( StashAnalyticsListener { audio, video -> audioDecoder = audio videoDecoder = video }, ) - StashExoPlayer.addListener( + player.addListener( object : Player.Listener { override fun onPlayerError(error: PlaybackException) { Timber.e( @@ -1155,12 +1154,13 @@ fun Player.setupFinishedBehavior( } PlaybackFinishBehavior.GO_BACK -> { - StashExoPlayer.addListener( + addListener( object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { if (playbackState == Player.STATE_ENDED) { navigationManager.goBack() + removeListener(this) } } }, @@ -1168,7 +1168,7 @@ fun Player.setupFinishedBehavior( } PlaybackFinishBehavior.DO_NOTHING -> { - StashExoPlayer.addListener( + addListener( object : Player.Listener { override fun onPlaybackStateChanged(playbackState: Int) { diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt index e576e0df..463a941d 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/ApplicationContent.kt @@ -27,7 +27,6 @@ import com.github.damontecres.stashapp.navigation.FilterAndPosition import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig -import com.github.damontecres.stashapp.ui.NavDrawerFragment.Companion.TAG import com.github.damontecres.stashapp.ui.compat.isTvDevice import com.github.damontecres.stashapp.ui.components.DefaultLongClicker import com.github.damontecres.stashapp.ui.components.DialogPopup @@ -149,7 +148,8 @@ fun ApplicationContent( if (fullScreen) { DestinationContent( currentServer = currentServer, - navManger = navigationManager, + preferences = preferences, + navManager = navigationManager, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, @@ -243,6 +243,7 @@ fun ApplicationContent( if (isTvDevice) { NavDrawer( + preferences = preferences, currentServer = currentServer, navigationManager = navigationManager, composeUiConfig = composeUiConfig, @@ -258,6 +259,7 @@ fun ApplicationContent( ) } else { NavScaffold( + preferences = preferences, currentServer = currentServer, navigationManager = navigationManager, composeUiConfig = composeUiConfig, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt index faae8d8c..b9f32810 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/DestinationContent.kt @@ -9,6 +9,7 @@ import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.di.server.StashServer 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.ComposeUiConfig import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.LongClicker @@ -40,6 +41,7 @@ import kotlin.time.Duration.Companion.seconds */ @Composable fun DestinationContent( + preferences: StashPreferences, currentServer: CurrentServer, navManager: NavigationManager, destination: Destination, @@ -113,6 +115,7 @@ fun DestinationContent( is Destination.Playback -> { PlaybackPage( + preferences = preferences, sceneId = destination.sceneId, startPosition = destination.position, playbackMode = destination.mode, @@ -124,6 +127,7 @@ fun DestinationContent( is Destination.Playlist -> { PlaylistPlaybackPage( + preferences = preferences, currentServer = currentServer, uiConfig = composeUiConfig, filterArgs = destination.filterArgs, @@ -297,8 +301,20 @@ fun DestinationContent( } } - else -> { - FragmentView(navManager, destination, modifier) + Destination.LicenseInfo -> { + TODO() + } + + is Destination.ReleaseChangelog -> { + TODO() + } + + is Destination.SearchFor -> { + TODO() + } + + Destination.Setup -> { + TODO() } } } diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt index 1b529d5d..fcc875b6 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavDrawer.kt @@ -39,6 +39,7 @@ import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.di.server.StashServer 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.ComposeUiConfig import com.github.damontecres.stashapp.ui.LocalPlayerContext import com.github.damontecres.stashapp.ui.PlayerContext @@ -55,6 +56,7 @@ import kotlinx.coroutines.launch @Composable fun NavDrawer( + preferences: StashPreferences, currentServer: CurrentServer, navigationManager: NavigationManager, composeUiConfig: ComposeUiConfig, @@ -204,8 +206,9 @@ fun NavDrawer( LocalPlayerContext provides PlayerContext, ) { DestinationContent( + preferences = preferences, currentServer = currentServer, - navManger = navigationManager, + navManager = navigationManager, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt index 0ae9ac8b..82cc7a29 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/nav/NavScaffold.kt @@ -38,6 +38,7 @@ import com.github.damontecres.stashapp.di.server.CurrentServer import com.github.damontecres.stashapp.di.server.StashServer 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.ComposeUiConfig import com.github.damontecres.stashapp.ui.FontAwesome import com.github.damontecres.stashapp.ui.components.ItemOnClicker @@ -48,6 +49,7 @@ import com.github.damontecres.stashapp.ui.util.screenSize @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) @Composable fun NavScaffold( + preferences: StashPreferences, currentServer: CurrentServer, navigationManager: NavigationManager, composeUiConfig: ComposeUiConfig, @@ -182,8 +184,9 @@ fun NavScaffold( verticalArrangement = Arrangement.spacedBy(16.dp), ) { DestinationContent( + preferences = preferences, currentServer = currentServer, - navManger = navigationManager, + navManager = navigationManager, destination = destination, composeUiConfig = composeUiConfig, itemOnClick = itemOnClick, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt index 1cc857d8..5f2a6c6e 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPage.kt @@ -15,11 +15,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import com.apollographql.apollo.api.Optional -import com.github.damontecres.stashapp.StashExoPlayer import com.github.damontecres.stashapp.api.fragment.FullMarkerData import com.github.damontecres.stashapp.api.fragment.StashData import com.github.damontecres.stashapp.api.fragment.VideoSceneData @@ -36,6 +36,7 @@ import com.github.damontecres.stashapp.playback.buildMediaItem import com.github.damontecres.stashapp.playback.getStreamDecision import com.github.damontecres.stashapp.proto.PlaybackBackend import com.github.damontecres.stashapp.proto.PlaybackPreferences +import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.suppliers.DataSupplierOverride import com.github.damontecres.stashapp.suppliers.FilterArgs import com.github.damontecres.stashapp.ui.ComposeUiConfig @@ -45,7 +46,6 @@ import com.github.damontecres.stashapp.ui.components.ItemOnClicker import com.github.damontecres.stashapp.ui.components.playback.PlaybackPageContent import com.github.damontecres.stashapp.util.AlphabetSearchUtils import com.github.damontecres.stashapp.util.LoggingCoroutineExceptionHandler -import com.github.damontecres.stashapp.util.SkipParams import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -56,6 +56,7 @@ import kotlin.time.Duration.Companion.seconds @Composable fun PlaybackPage( + preferences: StashPreferences, uiConfig: ComposeUiConfig, sceneId: String, startPosition: Long, @@ -82,27 +83,18 @@ fun PlaybackPage( state?.let { state -> val player = remember { - TODO() - val skipParams = - uiConfig.preferences.playbackPreferences.let { - SkipParams.Values( - it.skipForwardMs, - it.skipBackwardMs, - ) - } - val httpClient = uiConfig.preferences.playbackPreferences.playbackHttpClient - val debugLogging = uiConfig.preferences.playbackPreferences.debugLoggingEnabled - val backend = uiConfig.preferences.playbackPreferences.playbackBackend - StashExoPlayer - .getInstance( - context, - server, - uiConfig.preferences.playbackPreferences, - ).apply { + viewModel.playerFactory + .createPlayer(preferences.playbackPreferences) + .apply { repeatMode = Player.REPEAT_MODE_OFF playWhenReady = true } } + LifecycleResumeEffect(Unit) { + onPauseOrDispose { + player.release() + } + } val playbackScene = state.scene val decision = remember { @@ -179,6 +171,7 @@ const val PLAYLIST_PREFETCH = 15 @Composable fun PlaylistPlaybackPage( + preferences: StashPreferences, currentServer: CurrentServer, uiConfig: ComposeUiConfig, filterArgs: FilterArgs, @@ -226,16 +219,21 @@ fun PlaylistPlaybackPage( if (playlist.isNotEmpty()) { val player = remember { - StashExoPlayer - .getInstance(context, currentServer, uiConfig.preferences.playbackPreferences) + viewModel.playerFactory + .createPlayer(preferences.playbackPreferences) .apply { repeatMode = Player.REPEAT_MODE_OFF playWhenReady = true } } + LifecycleResumeEffect(Unit) { + onPauseOrDispose { + player.release() + } + } val mutex = remember { Mutex() } LaunchedEffect(Unit) { - StashExoPlayer.addListener( + player.addListener( object : Player.Listener { override fun onMediaItemTransition( mediaItem: MediaItem?, diff --git a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt index 23bc8118..54e3afdf 100644 --- a/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt +++ b/app/src/main/java/com/github/damontecres/stashapp/ui/pages/PlaybackPageViewModel.kt @@ -2,6 +2,7 @@ package com.github.damontecres.stashapp.ui.pages import android.util.Log import android.widget.Toast +import androidx.datastore.core.DataStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger @@ -10,7 +11,9 @@ import com.github.damontecres.stashapp.api.fragment.FullSceneData import com.github.damontecres.stashapp.data.Scene import com.github.damontecres.stashapp.di.server.QueryEngine import com.github.damontecres.stashapp.di.server.ServerRepository +import com.github.damontecres.stashapp.di.services.PlayerFactory import com.github.damontecres.stashapp.di.services.ServerLogger +import com.github.damontecres.stashapp.proto.StashPreferences import com.github.damontecres.stashapp.util.launchIO import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.flow.MutableStateFlow @@ -25,6 +28,8 @@ class PlaybackPageViewModel( private val serverLogger: ServerLogger, private val queryEngine: QueryEngine, @InjectedParam private val sceneId: String, + val preferences: DataStore, + val playerFactory: PlayerFactory, ) : ViewModel() { private val exceptionHandler = object : CoroutineExceptionHandler { From 3499a39456f008bd442b301a21e45dfd686b1d8e Mon Sep 17 00:00:00 2001 From: Damontecres Date: Mon, 18 May 2026 20:46:04 -0400 Subject: [PATCH 08/25] WIP remove leanback/old UI --- .../damontecres/stashapp/DebugFragment.kt | 382 --------- .../damontecres/stashapp/DetailsFragment.kt | 133 --- .../stashapp/FilterDebugFragment.kt | 93 -- .../damontecres/stashapp/FilterFragment.kt | 410 --------- .../stashapp/GalleryDetailsFragment.kt | 95 --- .../damontecres/stashapp/GalleryFragment.kt | 117 --- .../stashapp/GroupDetailsFragment.kt | 103 --- .../damontecres/stashapp/GroupFragment.kt | 162 ---- .../damontecres/stashapp/MainActivity.kt | 6 +- .../damontecres/stashapp/MainFragment.kt | 365 -------- .../stashapp/MarkerDetailsFragment.kt | 445 ---------- .../stashapp/PerformerDetailsFragment.kt | 149 ---- .../damontecres/stashapp/PerformerFragment.kt | 309 ------- .../damontecres/stashapp/PinFragment.kt | 2 +- .../damontecres/stashapp/RootActivity.kt | 11 +- .../stashapp/SceneDetailsFragment.kt | 800 ------------------ .../damontecres/stashapp/SearchForFragment.kt | 520 ------------ .../damontecres/stashapp/SettingsFragment.kt | 786 ----------------- .../stashapp/SettingsUiFragment.kt | 153 ---- .../damontecres/stashapp/StashApplication.kt | 7 - .../stashapp/StashDataGridFragment.kt | 694 --------------- .../damontecres/stashapp/StashExoPlayer.kt | 299 ------- .../stashapp/StashGridControlsFragment.kt | 326 ------- .../stashapp/StashSearchFragment.kt | 146 ---- .../stashapp/StudioDetailsFragment.kt | 117 --- .../damontecres/stashapp/StudioFragment.kt | 191 ----- .../damontecres/stashapp/TabbedFragment.kt | 151 ---- .../stashapp/TagDetailsFragment.kt | 93 -- .../damontecres/stashapp/TagFragment.kt | 181 ---- .../damontecres/stashapp/UpdateAppFragment.kt | 118 --- .../stashapp/UpdateChangelogFragment.kt | 31 - .../stashapp/di/server/MutationEngine.kt | 9 +- .../stashapp/di/server/QueryEngine.kt | 5 - .../stashapp/di/server/ServerPreferences.kt | 14 +- .../stashapp/di/server/ServerRepository.kt | 31 +- .../stashapp/di/server/StashApi.kt | 65 +- .../stashapp/di/server/StashServer.kt | 2 + .../stashapp/di/services/PlayerFactory.kt | 52 ++ .../stashapp/filter/CreateFilterFragment.kt | 45 - .../filter/CreateFilterGuidedStepFragment.kt | 108 --- .../stashapp/filter/CreateFilterStep.kt | 242 ------ .../stashapp/filter/CreateFilterViewModel.kt | 2 +- .../filter/CreateFindFilterFragment.kt | 162 ---- .../stashapp/filter/CreateObjectFilterStep.kt | 228 ----- .../stashapp/filter/DescriptionExtractors.kt | 13 +- .../filter/StashGuidedActionsStylist.kt | 60 -- .../filter/picker/BooleanPickerFragment.kt | 82 -- .../picker/CircumcisionPickerFragment.kt | 135 --- .../filter/picker/DatePickerFragment.kt | 169 ---- .../filter/picker/DurationPickerFragment.kt | 119 --- .../filter/picker/FloatPickerFragment.kt | 54 -- .../filter/picker/GenderPickerFragment.kt | 138 --- .../picker/GuidedDurationPickerAction.kt | 48 -- .../HierarchicalMultiCriterionFragment.kt | 265 ------ .../filter/picker/IntPickerFragment.kt | 51 -- .../filter/picker/MultiCriterionFragment.kt | 228 ----- .../picker/OrientationPickerFragment.kt | 83 -- .../filter/picker/RatingPickerFragment.kt | 89 -- .../filter/picker/ResolutionPickerFragment.kt | 117 --- .../filter/picker/SearchPickerFragment.kt | 305 ------- .../filter/picker/StringPickerFragment.kt | 131 --- .../stashapp/filter/picker/TwoValuePicker.kt | 192 ----- .../stashapp/image/ImageClipFragment.kt | 146 ---- .../stashapp/image/ImageController.kt | 29 - .../stashapp/image/ImageDetailsFragment.kt | 637 -------------- .../stashapp/image/ImageFragment.kt | 266 ------ .../stashapp/image/ImageViewFragment.kt | 372 -------- .../stashapp/navigation/Destination.kt | 33 +- .../stashapp/navigation/NavigationManager.kt | 317 ------- .../navigation/NavigationManagerCompose.kt | 233 ----- .../NavigationOnItemViewClickedListener.kt | 84 -- .../ControllerVisibilityListenerList.kt | 22 - .../stashapp/playback/PlaybackFragment.kt | 740 ---------------- .../playback/PlaybackSceneFragment.kt | 157 ---- .../playback/PlaybackVideoFiltersFragment.kt | 171 ---- .../stashapp/playback/PlaylistFragment.kt | 334 -------- .../stashapp/playback/PlaylistListFragment.kt | 122 --- .../playback/PlaylistMarkersFragment.kt | 72 -- .../playback/PlaylistScenesFragment.kt | 34 - .../stashapp/playback/PlaylistViewModel.kt | 50 -- .../stashapp/playback/StashPlayerView.kt | 95 --- .../stashapp/playback/StreamUtils.kt | 36 +- .../stashapp/playback/VideoFilterViewModel.kt | 104 --- .../stashapp/presenters/ActionPresenter.kt | 22 - .../presenters/ClassPresenterSelector.kt | 78 -- .../presenters/CreateMarkerActionPresenter.kt | 20 - .../presenters/FilterArgsPresenter.kt | 87 -- .../stashapp/presenters/GalleryPresenter.kt | 48 -- .../stashapp/presenters/GroupPresenter.kt | 74 -- .../presenters/GroupRelationshipPresenter.kt | 19 - .../stashapp/presenters/ImagePresenter.kt | 61 -- .../stashapp/presenters/MarkerPresenter.kt | 84 -- .../stashapp/presenters/NullPresenter.kt | 26 - .../presenters/NullPresenterSelector.kt | 19 - .../stashapp/presenters/OCounterPresenter.kt | 30 - .../presenters/PerformerInScenePresenter.kt | 43 - .../stashapp/presenters/PerformerPresenter.kt | 80 -- .../presenters/PlaylistItemPresenter.kt | 77 -- .../presenters/PopupOnLongClickListener.kt | 57 -- .../presenters/SceneDetailsPresenter.kt | 170 ---- .../stashapp/presenters/ScenePresenter.kt | 124 --- .../stashapp/presenters/StashImageCardView.kt | 538 ------------ .../stashapp/presenters/StashPresenter.kt | 259 ------ .../stashapp/presenters/StudioPresenter.kt | 66 -- .../stashapp/presenters/TagPresenter.kt | 130 --- .../stashapp/setup/ConfigureServerStep.kt | 223 ----- .../stashapp/setup/ManageServersFragment.kt | 169 ---- .../stashapp/setup/SetupFragment.kt | 41 - .../setup/SetupGuidedStepSupportFragment.kt | 60 -- .../damontecres/stashapp/setup/SetupState.kt | 14 - .../stashapp/setup/SetupStep1ServerUrl.kt | 114 --- .../stashapp/setup/SetupStep2Ssl.kt | 82 -- .../stashapp/setup/SetupStep3ApiKey.kt | 182 ---- .../stashapp/setup/SetupStep4Pin.kt | 127 --- .../readonly/ReadOnlyPinConfigFragment.kt | 118 --- .../readonly/SettingsPinEntryFragment.kt | 77 -- .../stashapp/suppliers/StashPagingSource.kt | 2 +- .../stashapp/ui/ComposeUiConfig.kt | 2 + .../damontecres/stashapp/ui/Extensions.kt | 23 +- .../stashapp/ui/FilterViewModel.kt | 4 +- .../damontecres/stashapp/ui/cards/Cards.kt | 32 +- .../stashapp/ui/cards/FilterCards.kt | 40 +- .../stashapp/ui/cards/GalleryCard.kt | 69 +- .../stashapp/ui/cards/GroupCard.kt | 5 +- .../stashapp/ui/cards/ImageCard.kt | 5 +- .../stashapp/ui/cards/MarkerCard.kt | 5 +- .../stashapp/ui/cards/PerformerCard.kt | 5 +- .../stashapp/ui/cards/SceneCard.kt | 7 +- .../stashapp/ui/cards/StudioCard.kt | 5 +- .../damontecres/stashapp/ui/cards/TagCard.kt | 5 +- .../stashapp/ui/components/ItemsRow.kt | 7 +- .../ui/components/SavedFiltersButton.kt | 23 +- .../stashapp/ui/components/StashGrid.kt | 5 +- .../components/image/ImageDetailsViewModel.kt | 4 +- .../playback/PlaybackPageContent.kt | 16 +- .../components/prefs/ComposablePreference.kt | 3 +- .../ui/components/prefs/PreferencesContent.kt | 2 +- .../components/prefs/PreferencesViewModel.kt | 19 +- .../ui/components/prefs/StashPreference.kt | 2 +- .../components/scene/SceneDetailsViewModel.kt | 2 +- .../screensaver/ChooseScreensaverFilter.kt | 25 +- .../ui/components/server/AddServerContent.kt | 2 +- .../ui/components/server/InitialSetup.kt | 2 +- .../ui/components/server/ManageServers.kt | 8 +- .../server/ManageServersViewModel.kt | 68 +- .../stashapp/ui/nav/ApplicationContent.kt | 11 +- .../stashapp/ui/nav/FragmentView.kt | 32 - .../damontecres/stashapp/ui/nav/NavDrawer.kt | 219 ++--- .../stashapp/ui/pages/ChooseThemePage.kt | 3 +- .../stashapp/ui/pages/DebugPage.kt | 4 +- .../stashapp/ui/pages/GalleryPage.kt | 4 +- .../stashapp/ui/pages/GroupPage.kt | 4 +- .../stashapp/ui/pages/ImagePage.kt | 4 +- .../damontecres/stashapp/ui/pages/MainPage.kt | 3 +- .../stashapp/ui/pages/MarkerTimestampPage.kt | 10 +- .../stashapp/ui/pages/PlaybackPage.kt | 8 +- .../stashapp/ui/pages/SearchForPage.kt | 112 ++- .../stashapp/ui/pages/SearchPage.kt | 2 +- .../stashapp/ui/pages/SettingsPage.kt | 2 +- .../stashapp/ui/pages/StudioPage.kt | 4 +- .../damontecres/stashapp/ui/pages/TagPage.kt | 4 +- .../stashapp/ui/util/ModifierUtils.kt | 5 +- .../stashapp/util/AlphabetSearchUtils.kt | 1 + .../damontecres/stashapp/util/Constants.kt | 146 +--- .../stashapp/util/FrontPageParser.kt | 2 +- .../stashapp/util/ListRowManager.kt | 158 ---- .../util/LoggingCoroutineExceptionHandler.kt | 5 +- .../stashapp/util/MutationEngine.kt | 579 ------------- .../util/OCounterLongClickCallBack.kt | 117 --- .../stashapp/util/PreferenceScreenOption.kt | 15 + .../damontecres/stashapp/util/QueryEngine.kt | 526 ------------ .../stashapp/util/RemoveLongClickListener.kt | 32 - .../stashapp/util/ServerPreferences.kt | 506 ----------- .../stashapp/util/SingleItemObjectAdapter.kt | 27 - .../damontecres/stashapp/util/StashClient.kt | 287 +------ .../stashapp/util/StashDreamService.kt | 1 + .../damontecres/stashapp/util/StashEngine.kt | 66 -- .../damontecres/stashapp/util/StashServer.kt | 195 ----- .../stashapp/util/SubscriptionEngine.kt | 66 -- ...ionPlugin.kt => CompanionPluginService.kt} | 195 ++--- .../util/plugin/CrashReportSenderFactory.kt | 24 +- .../damontecres/stashapp/views/Formatting.kt | 3 +- .../stashapp/views/HomeImageButton.kt | 30 - .../stashapp/views/MainTitleView.kt | 179 ---- .../stashapp/views/MarkerPickerFragment.kt | 226 ----- .../stashapp/views/PlayAllOnClickListener.kt | 83 -- .../stashapp/views/SimpleListPopupWindow.kt | 75 -- .../stashapp/views/StashRatingBar.kt | 201 ----- .../stashapp/views/models/GalleryViewModel.kt | 11 - .../stashapp/views/models/GroupViewModel.kt | 11 - .../stashapp/views/models/ImageViewModel.kt | 241 ------ .../stashapp/views/models/ItemViewModel.kt | 51 -- .../views/models/MarkerDetailsViewModel.kt | 2 + .../views/models/PerformerViewModel.kt | 11 - .../views/models/PlaybackViewModel.kt | 24 - .../stashapp/views/models/SceneViewModel.kt | 136 --- .../stashapp/views/models/ServerViewModel.kt | 4 +- .../views/models/StashGridViewModel.kt | 195 ----- .../stashapp/views/models/StudioViewModel.kt | 11 - .../views/models/TabbedGridViewModel.kt | 14 - .../stashapp/views/models/TagViewModel.kt | 11 - .../stashapp/FrontPageFilterTests.kt | 1 - 202 files changed, 776 insertions(+), 22532 deletions(-) delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/DebugFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/DetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/FilterDebugFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/FilterFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/GalleryDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/GalleryFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/GroupDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/GroupFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/MainFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/MarkerDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/PerformerDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/PerformerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/SceneDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/SearchForFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/SettingsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/SettingsUiFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/StashDataGridFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/StashExoPlayer.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/StashGridControlsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/StashSearchFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/StudioDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/StudioFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/TabbedFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/TagDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/TagFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/UpdateAppFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/UpdateChangelogFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterGuidedStepFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/CreateFilterStep.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/CreateFindFilterFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/CreateObjectFilterStep.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/StashGuidedActionsStylist.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/BooleanPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/CircumcisionPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/DatePickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/DurationPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/FloatPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/GenderPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/GuidedDurationPickerAction.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/HierarchicalMultiCriterionFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/IntPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/MultiCriterionFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/OrientationPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/RatingPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/ResolutionPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/SearchPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/StringPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/filter/picker/TwoValuePicker.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/image/ImageClipFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/image/ImageController.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/image/ImageDetailsFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/image/ImageFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/image/ImageViewFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/navigation/NavigationManager.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/navigation/NavigationManagerCompose.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/navigation/NavigationOnItemViewClickedListener.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/ControllerVisibilityListenerList.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaybackFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaybackSceneFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaybackVideoFiltersFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaylistFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaylistListFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaylistMarkersFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaylistScenesFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/PlaylistViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/StashPlayerView.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/playback/VideoFilterViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/ActionPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/ClassPresenterSelector.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/CreateMarkerActionPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/FilterArgsPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/GalleryPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/GroupPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/GroupRelationshipPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/ImagePresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/MarkerPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/NullPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/NullPresenterSelector.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/OCounterPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/PerformerInScenePresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/PerformerPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/PlaylistItemPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/PopupOnLongClickListener.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/SceneDetailsPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/ScenePresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/StashImageCardView.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/StashPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/StudioPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/presenters/TagPresenter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/ConfigureServerStep.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/ManageServersFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/SetupFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/SetupGuidedStepSupportFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/SetupState.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/SetupStep1ServerUrl.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/SetupStep2Ssl.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/SetupStep3ApiKey.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/SetupStep4Pin.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/readonly/ReadOnlyPinConfigFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/setup/readonly/SettingsPinEntryFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/ui/nav/FragmentView.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/ListRowManager.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/MutationEngine.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/OCounterLongClickCallBack.kt create mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/PreferenceScreenOption.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/QueryEngine.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/RemoveLongClickListener.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/ServerPreferences.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/SingleItemObjectAdapter.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/StashEngine.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/StashServer.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/util/SubscriptionEngine.kt rename app/src/main/java/com/github/damontecres/stashapp/util/plugin/{CompanionPlugin.kt => CompanionPluginService.kt} (69%) delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/HomeImageButton.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/MainTitleView.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/MarkerPickerFragment.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/PlayAllOnClickListener.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/SimpleListPopupWindow.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/StashRatingBar.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/GalleryViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/GroupViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/ImageViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/ItemViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/PerformerViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/PlaybackViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/SceneViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/StashGridViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/StudioViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/TabbedGridViewModel.kt delete mode 100644 app/src/main/java/com/github/damontecres/stashapp/views/models/TagViewModel.kt 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 068ba063..00000000 --- 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