diff --git a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/consts.kt b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/consts.kt index 83fc01cb8..7151346b8 100644 --- a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/consts.kt +++ b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/consts.kt @@ -1,6 +1,7 @@ package dev.alvr.katana.buildlogic internal const val AndroidDir = "src/androidMain" +internal const val ResourcesDir = "src/commonMain/resources" internal const val ANDROID_APPLICATION_PLUGIN = "com.android.application" internal const val ANDROID_LIBRARY_PLUGIN = "com.android.library" diff --git a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/extensions.kt b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/extensions.kt index d35f27fb5..324bba8fb 100644 --- a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/extensions.kt +++ b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/extensions.kt @@ -16,6 +16,7 @@ import org.gradle.jvm.toolchain.JvmVendorSpec import org.gradle.kotlin.dsl.DependencyHandlerScope import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.dependencies +import org.gradle.kotlin.dsl.get import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.dsl.KotlinCommonCompilerOptions @@ -89,6 +90,11 @@ internal fun BaseExtension.configureAndroid(packageName: String) { targetCompatibility = KatanaConfiguration.UseJavaVersion } + with(sourceSets["main"]) { + res.srcDirs("$AndroidDir/res", ResourcesDir) + resources.srcDirs(ResourcesDir) + } + testOptions { animationsDisabled = true unitTests { diff --git a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/app/KatanaMultiplatformAppPlugin.kt b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/app/KatanaMultiplatformAppPlugin.kt index 10ba6bac4..862d3d6c9 100644 --- a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/app/KatanaMultiplatformAppPlugin.kt +++ b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/app/KatanaMultiplatformAppPlugin.kt @@ -5,6 +5,7 @@ import com.android.build.gradle.internal.dsl.BaseAppModuleExtension import dev.alvr.katana.buildlogic.ANDROID_APPLICATION_PLUGIN import dev.alvr.katana.buildlogic.AndroidDir import dev.alvr.katana.buildlogic.KatanaConfiguration +import dev.alvr.katana.buildlogic.ResourcesDir import dev.alvr.katana.buildlogic.catalogBundle import dev.alvr.katana.buildlogic.configureAndroid import dev.alvr.katana.buildlogic.mp.mobile.KatanaMultiplatformMobileBasePlugin @@ -119,6 +120,7 @@ internal class KatanaMultiplatformAppPlugin : KatanaMultiplatformMobileBasePlugi sourceSets["main"].manifest.srcFile("$AndroidDir/AndroidManifest.xml") sourceSets["main"].res.srcDirs("$AndroidDir/res") + sourceSets["main"].resources.srcDirs(ResourcesDir) } private fun SentryPluginExtension.configureSentry() { diff --git a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/ui/KatanaMultiplatformComposePlugin.kt b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/ui/KatanaMultiplatformComposePlugin.kt index 680aded56..db1fb3da9 100644 --- a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/ui/KatanaMultiplatformComposePlugin.kt +++ b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/mobile/ui/KatanaMultiplatformComposePlugin.kt @@ -1,17 +1,22 @@ package dev.alvr.katana.buildlogic.mp.mobile.ui +import dev.alvr.katana.buildlogic.ResourcesDir import dev.alvr.katana.buildlogic.catalogBundle import dev.alvr.katana.buildlogic.catalogLib import dev.alvr.katana.buildlogic.fullPackageName import dev.alvr.katana.buildlogic.kspDependencies import dev.alvr.katana.buildlogic.mp.androidUnitTest import dev.alvr.katana.buildlogic.mp.configureSourceSets +import dev.alvr.katana.buildlogic.mp.tasks.GenerateResourcesFileTask import java.io.File import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getValue +import org.gradle.kotlin.dsl.provideDelegate +import org.gradle.kotlin.dsl.registering import org.jetbrains.compose.ComposeExtension import org.jetbrains.compose.ComposePlugin import org.jetbrains.compose.ExperimentalComposeLibrary @@ -28,6 +33,8 @@ internal class KatanaMultiplatformComposePlugin : Plugin { configure { configureMultiplatform() } configure { configureComposeMultiplatform() } } + + generateResourcesTask() } context(Project) @@ -43,6 +50,8 @@ internal class KatanaMultiplatformComposePlugin : Plugin { configureSourceSets { commonMain { + kotlin.srcDirs("build/$GeneratedResourcesDir") + dependencies { implementation(compose.animation) implementation(compose.components.resources) @@ -93,9 +102,27 @@ internal class KatanaMultiplatformComposePlugin : Plugin { ) } + private fun Project.generateResourcesTask() { + val generateResourcesFile by tasks.registering(GenerateResourcesFileTask::class) { + packageName.set(fullPackageName) + inputFiles.from( + layout.projectDirectory.dir(ResourcesDir).asFileTree.matching { + include("**/*.webp", "**/*.xml") + }, + ) + outputDir.set(layout.buildDirectory.dir(GeneratedResourcesDir)) + } + + tasks.named("preBuild").configure { dependsOn(generateResourcesFile) } + } + private fun Project.composePluginEnabled(property: String) = providers.gradleProperty(property).orNull == "true" private fun Project.composePluginDir(directory: String) = File(layout.buildDirectory.asFile.get(), directory).absolutePath + + private companion object { + const val GeneratedResourcesDir = "generated/sources/katana/main" + } } diff --git a/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/tasks/GenerateResourcesFileTask.kt b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/tasks/GenerateResourcesFileTask.kt new file mode 100644 index 000000000..11dbac70a --- /dev/null +++ b/build-logic/katana-convention/src/main/kotlin/dev/alvr/katana/buildlogic/mp/tasks/GenerateResourcesFileTask.kt @@ -0,0 +1,87 @@ +package dev.alvr.katana.buildlogic.mp.tasks + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import dev.alvr.katana.buildlogic.ResourcesDir +import dev.alvr.katana.buildlogic.mp.identifier +import java.io.File +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.internal.FileUtils + +@CacheableTask +internal abstract class GenerateResourcesFileTask : DefaultTask() { + @get:Input + internal abstract val packageName: Property + + @get:InputFiles + @get:PathSensitive(value = PathSensitivity.RELATIVE) + internal abstract val inputFiles: ConfigurableFileCollection + + @get:OutputDirectory + internal abstract val outputDir: DirectoryProperty + + private val String.resourceIdentifier + get() = split('_').identifier + + @TaskAction + private fun generateResourcesFile() { + inputFiles.files.generateResourcesFile() + } + + private fun Set.generateResourcesFile() { + if (isEmpty()) { + outputDir.get().asFile.deleteRecursively() + return + } + + val stableAnnotation = AnnotationSpec + .builder(ClassName(ComposeRuntimePackage, ComposeStableAnnotation)) + .build() + + val properties = map { file -> + val propertyName = FileUtils.removeExtension(file.name).resourceIdentifier + val fileName = file.absolutePath.replace('\\', '/').substringAfter("$ResourcesDir/") + + PropertySpec.builder(propertyName, ClassName(KatanaResourcePackage, KatanaResourceClass)) + .addAnnotation(stableAnnotation) + .addModifiers(KModifier.INTERNAL) + .initializer("%L(%S)", KatanaResourceClass, fileName) + .build() + } + + val resourcesObject = TypeSpec.objectBuilder(KatanaResourcesLocalClass) + .addModifiers(KModifier.INTERNAL) + .addProperties(properties) + .build() + + FileSpec.builder(ClassName("${packageName.get()}$KatanaResourcesLocalPackage", KatanaResourcesLocalClass)) + .addType(resourcesObject) + .build() + .writeTo(outputDir.get().asFile) + } + + private companion object { + const val ComposeRuntimePackage = "androidx.compose.runtime" + const val ComposeStableAnnotation = "Stable" + + const val KatanaResourcePackage = "dev.alvr.katana.core.ui.resources" + const val KatanaResourceClass = "KatanaResource" + + const val KatanaResourcesLocalPackage = ".resources" + const val KatanaResourcesLocalClass = "KatanaResources" + } +} diff --git a/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/resources/KatanaResource.kt b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/resources/KatanaResource.kt new file mode 100644 index 000000000..097f90c9d --- /dev/null +++ b/core/ui/src/commonMain/kotlin/dev/alvr/katana/core/ui/resources/KatanaResource.kt @@ -0,0 +1,32 @@ +package dev.alvr.katana.core.ui.resources + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.saveable.Saver +import kotlin.jvm.JvmInline +import kotlinx.coroutines.runBlocking +import org.jetbrains.compose.resources.DrawableResource +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.InternalResourceApi +import org.jetbrains.compose.resources.painterResource +import org.jetbrains.compose.resources.readResourceBytes + +@Immutable +@JvmInline +value class KatanaResource(private val key: String) { + + @OptIn(ExperimentalResourceApi::class) + val asPainter @Composable get() = painterResource(DrawableResource(key)) + + @OptIn(ExperimentalResourceApi::class, InternalResourceApi::class) + val asByteArray get() = runBlocking { readResourceBytes(key) } + + val id get() = key.substringAfterLast('/').substringBeforeLast('.') + + companion object { + val saver = Saver( + save = { res -> res.key }, + restore = { key -> KatanaResource(key) } + ) + } +} diff --git a/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/view/components/MediaList.kt b/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/view/components/MediaList.kt index 520f8ba51..7a206ff80 100644 --- a/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/view/components/MediaList.kt +++ b/features/lists/ui/src/commonMain/kotlin/dev/alvr/katana/features/lists/ui/view/components/MediaList.kt @@ -50,8 +50,7 @@ import dev.alvr.katana.core.common.zero import dev.alvr.katana.core.ui.components.KatanaPullRefresh import dev.alvr.katana.core.ui.modifiers.katanaPlaceholder import dev.alvr.katana.features.lists.ui.entities.MediaListItem -import dev.alvr.katana.features.lists.ui.generated.resources.Res -import dev.alvr.katana.features.lists.ui.generated.resources.default_cover +import dev.alvr.katana.features.lists.ui.resources.KatanaResources import dev.alvr.katana.features.lists.ui.strings.LocalListsStrings import dev.alvr.katana.features.lists.ui.viewmodel.ListState import io.kamel.image.KamelImage @@ -266,7 +265,7 @@ private fun Cover( onFailure = { Image( modifier = Modifier.align(Alignment.Center), - painter = painterResource(Res.drawable.default_cover), + painter = KatanaResources.defaultCover.asPainter, contentDescription = LocalListsStrings.current.errorCover, ) }, diff --git a/features/lists/ui/src/commonMain/composeResources/drawable/default_cover.webp b/features/lists/ui/src/commonMain/resources/drawable/default_cover.webp similarity index 100% rename from features/lists/ui/src/commonMain/composeResources/drawable/default_cover.webp rename to features/lists/ui/src/commonMain/resources/drawable/default_cover.webp diff --git a/features/login/ui/src/androidMain/kotlin/dev/alvr/katana/features/login/ui/view/LoginScreen.kt b/features/login/ui/src/androidMain/kotlin/dev/alvr/katana/features/login/ui/view/LoginScreen.kt index ef0be1001..5f237f668 100644 --- a/features/login/ui/src/androidMain/kotlin/dev/alvr/katana/features/login/ui/view/LoginScreen.kt +++ b/features/login/ui/src/androidMain/kotlin/dev/alvr/katana/features/login/ui/view/LoginScreen.kt @@ -78,11 +78,7 @@ import dev.alvr.katana.features.login.ui.LOGIN_DEEP_LINK import dev.alvr.katana.features.login.ui.LOGO_FULL_SIZE import dev.alvr.katana.features.login.ui.LOGO_RESIZED import dev.alvr.katana.features.login.ui.generated.resources.Res -import dev.alvr.katana.features.login.ui.generated.resources.background_chihiro -import dev.alvr.katana.features.login.ui.generated.resources.background_howl -import dev.alvr.katana.features.login.ui.generated.resources.background_mononoke -import dev.alvr.katana.features.login.ui.generated.resources.background_totoro -import dev.alvr.katana.features.login.ui.generated.resources.ic_katana_logo +import dev.alvr.katana.features.login.ui.resources.KatanaResources import dev.alvr.katana.features.login.ui.navigation.LoginNavigator import dev.alvr.katana.features.login.ui.strings.LocalLoginStrings import dev.alvr.katana.features.login.ui.viewmodel.LoginState @@ -118,10 +114,10 @@ private fun Login(state: LoginState, onLogin: () -> Unit) { val background = remember { listOf( - Res.drawable.background_chihiro, - Res.drawable.background_howl, - Res.drawable.background_mononoke, - Res.drawable.background_totoro, + KatanaResources.backgroundChihiro, + KatanaResources.backgroundHowl, + KatanaResources.backgroundMononoke, + KatanaResources.backgroundTotoro, ).random() } @@ -153,7 +149,7 @@ private fun Login(state: LoginState, onLogin: () -> Unit) { Loading() } else { Image( - painter = painterResource(background), + painter = background.asPainter, contentDescription = strings.contentDescriptionBackground, contentScale = ContentScale.Crop, modifier = Modifier @@ -202,7 +198,7 @@ private fun KatanaLogo() { } Image( - painter = painterResource(Res.drawable.ic_katana_logo), + painter = KatanaResources.icKatanaLogo.asPainter, contentDescription = LocalLoginStrings.current.contentDescriptionKatanaLogo, modifier = Modifier .padding(top = 8.dp) diff --git a/features/login/ui/src/commonMain/composeResources/drawable/background_chihiro.webp b/features/login/ui/src/commonMain/resources/drawable/background_chihiro.webp similarity index 100% rename from features/login/ui/src/commonMain/composeResources/drawable/background_chihiro.webp rename to features/login/ui/src/commonMain/resources/drawable/background_chihiro.webp diff --git a/features/login/ui/src/commonMain/composeResources/drawable/background_howl.webp b/features/login/ui/src/commonMain/resources/drawable/background_howl.webp similarity index 100% rename from features/login/ui/src/commonMain/composeResources/drawable/background_howl.webp rename to features/login/ui/src/commonMain/resources/drawable/background_howl.webp diff --git a/features/login/ui/src/commonMain/composeResources/drawable/background_mononoke.webp b/features/login/ui/src/commonMain/resources/drawable/background_mononoke.webp similarity index 100% rename from features/login/ui/src/commonMain/composeResources/drawable/background_mononoke.webp rename to features/login/ui/src/commonMain/resources/drawable/background_mononoke.webp diff --git a/features/login/ui/src/commonMain/composeResources/drawable/background_totoro.webp b/features/login/ui/src/commonMain/resources/drawable/background_totoro.webp similarity index 100% rename from features/login/ui/src/commonMain/composeResources/drawable/background_totoro.webp rename to features/login/ui/src/commonMain/resources/drawable/background_totoro.webp diff --git a/features/login/ui/src/commonMain/composeResources/drawable/ic_katana_logo.xml b/features/login/ui/src/commonMain/resources/drawable/ic_katana_logo.xml similarity index 100% rename from features/login/ui/src/commonMain/composeResources/drawable/ic_katana_logo.xml rename to features/login/ui/src/commonMain/resources/drawable/ic_katana_logo.xml