Browse Source

Get resource files as URI (#4576)

Adds a public `Res.getUri(path: String): String` function.
It lets external libraries a way to read resource files by a platform
dependent Uri.
E.g.: video players, image loaders or embedded web browsers.

```kotlin
val uri = Res.getUri("files/my_video.mp4")
```

fixes https://github.com/JetBrains/compose-multiplatform/issues/4360
pull/4585/head
Konstantin 8 months ago committed by GitHub
parent
commit
d87aa7f0de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 24
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt
  2. 10
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt
  3. 18
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt
  4. 4
      components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt
  5. 13
      components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt
  6. 4
      components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt
  7. 5
      components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt
  8. 4
      components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt
  9. 5
      components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt
  10. 10
      components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt
  11. 21
      gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GeneratedResClassSpec.kt
  12. 11
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt
  13. 11
      gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt
  14. 13
      gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt
  15. 13
      gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

24
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/ResourceReader.android.kt

@ -19,15 +19,35 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
return result return result
} }
@OptIn(ExperimentalResourceApi::class)
override fun getUri(path: String): String {
val classLoader = getClassLoader()
val resource = classLoader.getResource(path) ?: run {
//try to find a font in the android assets
if (File(path).isFontResource()) {
classLoader.getResource("assets/$path")
} else null
} ?: throw MissingResourceException(path)
return resource.toURI().toString()
}
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
private fun getResourceAsStream(path: String): InputStream { private fun getResourceAsStream(path: String): InputStream {
val classLoader = Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader val classLoader = getClassLoader()
val resource = classLoader.getResourceAsStream(path) ?: run { val resource = classLoader.getResourceAsStream(path) ?: run {
//try to find a font in the android assets //try to find a font in the android assets
if (File(path).parentFile?.name.orEmpty().startsWith("font")) { if (File(path).isFontResource()) {
classLoader.getResourceAsStream("assets/$path") classLoader.getResourceAsStream("assets/$path")
} else null } else null
} ?: throw MissingResourceException(path) } ?: throw MissingResourceException(path)
return resource return resource
} }
private fun File.isFontResource(): Boolean {
return this.parentFile?.name.orEmpty().startsWith("font")
}
private fun getClassLoader(): ClassLoader {
return Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader!!
}
} }

10
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt

@ -14,9 +14,19 @@ class MissingResourceException(path: String) : Exception("Missing resource with
@InternalResourceApi @InternalResourceApi
suspend fun readResourceBytes(path: String): ByteArray = DefaultResourceReader.read(path) suspend fun readResourceBytes(path: String): ByteArray = DefaultResourceReader.read(path)
/**
* Provides the platform dependent URI for a given resource path.
*
* @param path The path to the file in the resource's directory.
* @return The URI string of the specified resource.
*/
@InternalResourceApi
fun getResourceUri(path: String): String = DefaultResourceReader.getUri(path)
internal interface ResourceReader { internal interface ResourceReader {
suspend fun read(path: String): ByteArray suspend fun read(path: String): ByteArray
suspend fun readPart(path: String, offset: Long, size: Long): ByteArray suspend fun readPart(path: String, offset: Long, size: Long): ByteArray
fun getUri(path: String): String
} }
internal expect fun getPlatformResourceReader(): ResourceReader internal expect fun getPlatformResourceReader(): ResourceReader

18
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/ComposeResourceTest.kt

@ -6,6 +6,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.runComposeUiTest
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.jetbrains.skiko.URIManager
import kotlin.test.* import kotlin.test.*
@OptIn(ExperimentalTestApi::class, ExperimentalResourceApi::class, InternalResourceApi::class) @OptIn(ExperimentalTestApi::class, ExperimentalResourceApi::class, InternalResourceApi::class)
@ -286,4 +287,21 @@ class ComposeResourceTest {
bytes.decodeToString() bytes.decodeToString()
) )
} }
@Test
fun testGetResourceUri() = runComposeUiTest {
var uri1 = ""
var uri2 = ""
setContent {
CompositionLocalProvider(LocalComposeEnvironment provides TestComposeEnvironment) {
val resourceReader = LocalResourceReader.current
uri1 = resourceReader.getUri("1.png")
uri2 = resourceReader.getUri("2.png")
}
}
waitForIdle()
assertTrue(uri1.endsWith("/1.png"))
assertTrue(uri2.endsWith("/2.png"))
}
} }

4
components/resources/library/src/commonTest/kotlin/org/jetbrains/compose/resources/TestResourceReader.kt

@ -13,4 +13,8 @@ internal class TestResourceReader : ResourceReader {
readPathsList.add("$path/$offset-$size") readPathsList.add("$path/$offset-$size")
return DefaultResourceReader.readPart(path, offset, size) return DefaultResourceReader.readPart(path, offset, size)
} }
override fun getUri(path: String): String {
return DefaultResourceReader.getUri(path)
}
} }

13
components/resources/library/src/desktopMain/kotlin/org/jetbrains/compose/resources/ResourceReader.desktop.kt

@ -18,9 +18,20 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
return result return result
} }
@OptIn(ExperimentalResourceApi::class)
override fun getUri(path: String): String {
val classLoader = getClassLoader()
val resource = classLoader.getResource(path) ?: throw MissingResourceException(path)
return resource.toURI().toString()
}
@OptIn(ExperimentalResourceApi::class) @OptIn(ExperimentalResourceApi::class)
private fun getResourceAsStream(path: String): InputStream { private fun getResourceAsStream(path: String): InputStream {
val classLoader = Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader val classLoader = getClassLoader()
return classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path) return classLoader.getResourceAsStream(path) ?: throw MissingResourceException(path)
} }
private fun getClassLoader(): ClassLoader {
return Thread.currentThread().contextClassLoader ?: this.javaClass.classLoader!!
}
} }

4
components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt

@ -21,6 +21,10 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
} }
} }
override fun getUri(path: String): String {
return NSURL.fileURLWithPath(getPathInBundle(path)).toString()
}
private fun readData(path: String): NSData { private fun readData(path: String): NSData {
return NSFileManager.defaultManager().contentsAtPath(path) ?: throw MissingResourceException(path) return NSFileManager.defaultManager().contentsAtPath(path) ?: throw MissingResourceException(path)
} }

5
components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt

@ -18,6 +18,11 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
return part.asByteArray() return part.asByteArray()
} }
override fun getUri(path: String): String {
val location = window.location
return getResourceUrl(location.origin, location.pathname, path)
}
private suspend fun readAsBlob(path: String): Blob { private suspend fun readAsBlob(path: String): Blob {
val resPath = WebResourcesConfiguration.getResourcePath(path) val resPath = WebResourcesConfiguration.getResourcePath(path)
val response = window.fetch(resPath).await() val response = window.fetch(resPath).await()

4
components/resources/library/src/macosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.macos.kt

@ -21,6 +21,10 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
} }
} }
override fun getUri(path: String): String {
return NSURL.fileURLWithPath(getPathOnDisk(path)).toString()
}
private fun readData(path: String): NSData { private fun readData(path: String): NSData {
return NSFileManager.defaultManager().contentsAtPath(path) ?: throw MissingResourceException(path) return NSFileManager.defaultManager().contentsAtPath(path) ?: throw MissingResourceException(path)
} }

5
components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt

@ -33,6 +33,11 @@ internal actual fun getPlatformResourceReader(): ResourceReader = object : Resou
return part.asByteArray() return part.asByteArray()
} }
override fun getUri(path: String): String {
val location = window.location
return getResourceUrl(location.origin, location.pathname, path)
}
private suspend fun readAsBlob(path: String): Blob { private suspend fun readAsBlob(path: String): Blob {
val resPath = WebResourcesConfiguration.getResourcePath(path) val resPath = WebResourcesConfiguration.getResourcePath(path)
val response = window.fetch(resPath).await<Response>() val response = window.fetch(resPath).await<Response>()

10
components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt

@ -45,3 +45,13 @@ object WebResourcesConfiguration {
fun configureWebResources(configure: WebResourcesConfiguration.() -> Unit) { fun configureWebResources(configure: WebResourcesConfiguration.() -> Unit) {
WebResourcesConfiguration.configure() WebResourcesConfiguration.configure()
} }
@OptIn(ExperimentalResourceApi::class)
internal fun getResourceUrl(windowOrigin: String, windowPathname: String, resourcePath: String): String {
val path = WebResourcesConfiguration.getResourcePath(resourcePath)
return when {
path.startsWith("/") -> windowOrigin + path
path.startsWith("http://") || path.startsWith("https://") -> path
else -> windowOrigin + windowPathname + path
}
}

21
gradle-plugins/compose/src/main/kotlin/org/jetbrains/compose/resources/GeneratedResClassSpec.kt

@ -160,6 +160,27 @@ internal fun getResFileSpecs(
.addStatement("""return %M("$moduleDir" + path)""", readResourceBytes) .addStatement("""return %M("$moduleDir" + path)""", readResourceBytes)
.build() .build()
) )
//getUri
val getResourceUri = MemberName("org.jetbrains.compose.resources", "getResourceUri")
resObject.addFunction(
FunSpec.builder("getUri")
.addKdoc(
"""
Returns the URI string of the resource file at the specified path.
Example: `val uri = Res.getUri("files/key.bin")`
@param path The path of the file in the compose resource's directory.
@return The URI string of the file.
""".trimIndent()
)
.addParameter("path", String::class)
.returns(String::class)
.addStatement("""return %M("$moduleDir" + path)""", getResourceUri)
.build()
)
ResourceType.values().forEach { type -> ResourceType.values().forEach { type ->
resObject.addType(TypeSpec.objectBuilder(type.accessorName).build()) resObject.addType(TypeSpec.objectBuilder(type.accessorName).build())
} }

11
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected-open-res/Res.kt

@ -9,6 +9,7 @@ import kotlin.ByteArray
import kotlin.OptIn import kotlin.OptIn
import kotlin.String import kotlin.String
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.getResourceUri
import org.jetbrains.compose.resources.readResourceBytes import org.jetbrains.compose.resources.readResourceBytes
@ExperimentalResourceApi @ExperimentalResourceApi
@ -23,6 +24,16 @@ public object Res {
*/ */
public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path) public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path)
/**
* Returns the URI string of the resource file at the specified path.
*
* Example: `val uri = Res.getUri("files/key.bin")`
*
* @param path The path of the file in the compose resource's directory.
* @return The URI string of the file.
*/
public fun getUri(path: String): String = getResourceUri("" + path)
public object drawable public object drawable
public object string public object string

11
gradle-plugins/compose/src/test/test-projects/misc/commonResources/expected/Res.kt

@ -9,6 +9,7 @@ import kotlin.ByteArray
import kotlin.OptIn import kotlin.OptIn
import kotlin.String import kotlin.String
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.getResourceUri
import org.jetbrains.compose.resources.readResourceBytes import org.jetbrains.compose.resources.readResourceBytes
@ExperimentalResourceApi @ExperimentalResourceApi
@ -23,6 +24,16 @@ internal object Res {
*/ */
public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path) public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path)
/**
* Returns the URI string of the resource file at the specified path.
*
* Example: `val uri = Res.getUri("files/key.bin")`
*
* @param path The path of the file in the compose resource's directory.
* @return The URI string of the file.
*/
public fun getUri(path: String): String = getResourceUri("" + path)
public object drawable public object drawable
public object string public object string

13
gradle-plugins/compose/src/test/test-projects/misc/emptyResources/expected/Res.kt

@ -3,12 +3,13 @@
org.jetbrains.compose.resources.ExperimentalResourceApi::class, org.jetbrains.compose.resources.ExperimentalResourceApi::class,
) )
package app.group.empty_res.generated.resources package app.group.resources_test.generated.resources
import kotlin.ByteArray import kotlin.ByteArray
import kotlin.OptIn import kotlin.OptIn
import kotlin.String import kotlin.String
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.getResourceUri
import org.jetbrains.compose.resources.readResourceBytes import org.jetbrains.compose.resources.readResourceBytes
@ExperimentalResourceApi @ExperimentalResourceApi
@ -23,6 +24,16 @@ internal object Res {
*/ */
public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path) public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path)
/**
* Returns the URI string of the resource file at the specified path.
*
* Example: `val uri = Res.getUri("files/key.bin")`
*
* @param path The path of the file in the compose resource's directory.
* @return The URI string of the file.
*/
public fun getUri(path: String): String = getResourceUri("" + path)
public object drawable public object drawable
public object string public object string

13
gradle-plugins/compose/src/test/test-projects/misc/jvmOnlyResources/expected/Res.kt

@ -3,12 +3,13 @@
org.jetbrains.compose.resources.ExperimentalResourceApi::class, org.jetbrains.compose.resources.ExperimentalResourceApi::class,
) )
package me.app.jvmonlyresources.generated.resources package app.group.resources_test.generated.resources
import kotlin.ByteArray import kotlin.ByteArray
import kotlin.OptIn import kotlin.OptIn
import kotlin.String import kotlin.String
import org.jetbrains.compose.resources.ExperimentalResourceApi import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.getResourceUri
import org.jetbrains.compose.resources.readResourceBytes import org.jetbrains.compose.resources.readResourceBytes
@ExperimentalResourceApi @ExperimentalResourceApi
@ -23,6 +24,16 @@ internal object Res {
*/ */
public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path) public suspend fun readBytes(path: String): ByteArray = readResourceBytes("" + path)
/**
* Returns the URI string of the resource file at the specified path.
*
* Example: `val uri = Res.getUri("files/key.bin")`
*
* @param path The path of the file in the compose resource's directory.
* @return The URI string of the file.
*/
public fun getUri(path: String): String = getResourceUri("" + path)
public object drawable public object drawable
public object string public object string

Loading…
Cancel
Save