diff --git a/build.gradle.kts b/build.gradle.kts index ff2121f1..803258cd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,6 +7,7 @@ buildscript { repositories { google() mavenCentral() + gradlePluginPortal() } dependencies { @@ -15,6 +16,7 @@ buildscript { classpath(libs.com.android.tools.build.gradle) classpath(libs.org.jetbrains.kotlin.serialization) classpath(libs.com.squareup.sqldelight.gradle.plugin) + classpath(libs.dev.icerock.moko.resources.generator) } } diff --git a/data/network/build.gradle.kts b/data/network/build.gradle.kts index 45e1a8da..5e653c88 100644 --- a/data/network/build.gradle.kts +++ b/data/network/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("com.android.library") kotlin("plugin.serialization") id("com.diffplug.spotless") version "6.11.0" + id("dev.icerock.mobile.multiplatform-resources") } spotless { @@ -55,6 +56,7 @@ kotlin { implementation(libs.io.ktor.serialization.kotlinx.json) implementation(libs.io.ktor.client.content.negotiation) implementation(libs.org.jetbrains.kotlinx.serialization.json) + implementation(libs.dev.icerock.moko.resources.common) } } @@ -115,7 +117,13 @@ kotlin { implementation(libs.org.jetbrains.kotlin.test.common) implementation(libs.org.jetbrains.kotlin.test.annotations.common) implementation(libs.org.jetbrains.kotlinx.coroutines.test) + implementation(libs.dev.icerock.moko.resources.test) } } } } + +multiplatformResources { + multiplatformResourcesPackage = "social.androiddev.common.network" // required + multiplatformResourcesSourceSet = "commonTest" // optional, default "commonMain" +} \ No newline at end of file diff --git a/data/network/src/commonMain/kotlin/social/androiddev/common/network/MastodonApiImpl.kt b/data/network/src/commonMain/kotlin/social/androiddev/common/network/MastodonApiImpl.kt index 8771cd2f..a87cd657 100644 --- a/data/network/src/commonMain/kotlin/social/androiddev/common/network/MastodonApiImpl.kt +++ b/data/network/src/commonMain/kotlin/social/androiddev/common/network/MastodonApiImpl.kt @@ -13,6 +13,7 @@ import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.ResponseException import io.ktor.client.request.get +import io.ktor.serialization.* import kotlinx.serialization.SerializationException import social.androiddev.common.network.model.Instance @@ -33,6 +34,8 @@ class MastodonApiImpl( Result.failure(exception = exception) } catch (exception: ResponseException) { Result.failure(exception = exception) + } catch (exception: JsonConvertException) { + Result.failure(exception = exception) } } } diff --git a/data/network/src/commonTest/resources/response_account_required.json b/data/network/src/commonTest/resources/MR/files/response_account_required.json similarity index 100% rename from data/network/src/commonTest/resources/response_account_required.json rename to data/network/src/commonTest/resources/MR/files/response_account_required.json diff --git a/data/network/src/commonTest/resources/response_context_required.json b/data/network/src/commonTest/resources/MR/files/response_context_required.json similarity index 100% rename from data/network/src/commonTest/resources/response_context_required.json rename to data/network/src/commonTest/resources/MR/files/response_context_required.json diff --git a/data/network/src/commonTest/resources/response_conversation_required.json b/data/network/src/commonTest/resources/MR/files/response_conversation_required.json similarity index 100% rename from data/network/src/commonTest/resources/response_conversation_required.json rename to data/network/src/commonTest/resources/MR/files/response_conversation_required.json diff --git a/data/network/src/commonTest/resources/response_instance_invalid.json b/data/network/src/commonTest/resources/MR/files/response_instance_invalid.json similarity index 100% rename from data/network/src/commonTest/resources/response_instance_invalid.json rename to data/network/src/commonTest/resources/MR/files/response_instance_invalid.json diff --git a/data/network/src/commonTest/resources/response_instance_valid.json b/data/network/src/commonTest/resources/MR/files/response_instance_valid.json similarity index 100% rename from data/network/src/commonTest/resources/response_instance_valid.json rename to data/network/src/commonTest/resources/MR/files/response_instance_valid.json diff --git a/data/network/src/commonTest/resources/response_notification_required.json b/data/network/src/commonTest/resources/MR/files/response_notification_required.json similarity index 100% rename from data/network/src/commonTest/resources/response_notification_required.json rename to data/network/src/commonTest/resources/MR/files/response_notification_required.json diff --git a/data/network/src/commonTest/resources/response_status_required.json b/data/network/src/commonTest/resources/MR/files/response_status_required.json similarity index 100% rename from data/network/src/commonTest/resources/response_status_required.json rename to data/network/src/commonTest/resources/MR/files/response_status_required.json diff --git a/data/network/src/commonTest/kotlin/social/androiddev/common/network/MastodonApiTests.kt b/data/network/src/desktopTest/kotlin/social/androiddev/common/network/MastodonApiTests.kt similarity index 70% rename from data/network/src/commonTest/kotlin/social/androiddev/common/network/MastodonApiTests.kt rename to data/network/src/desktopTest/kotlin/social/androiddev/common/network/MastodonApiTests.kt index 577fb572..297caae2 100644 --- a/data/network/src/commonTest/kotlin/social/androiddev/common/network/MastodonApiTests.kt +++ b/data/network/src/desktopTest/kotlin/social/androiddev/common/network/MastodonApiTests.kt @@ -9,38 +9,34 @@ */ package social.androiddev.common.network -import io.ktor.client.HttpClient -import io.ktor.client.engine.mock.MockEngine -import io.ktor.client.engine.mock.respond -import io.ktor.client.plugins.contentnegotiation.ContentNegotiation -import io.ktor.http.ContentType -import io.ktor.http.HttpHeaders -import io.ktor.http.HttpStatusCode -import io.ktor.http.headersOf -import io.ktor.serialization.kotlinx.json.json -import io.ktor.utils.io.ByteReadChannel +import io.ktor.client.* +import io.ktor.client.engine.mock.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import io.ktor.utils.io.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json -import kotlin.test.Ignore -import kotlin.test.Test -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue +import kotlin.test.* +/** + * I was unable to read a .json file from commonTest, and so API tests must be done from + * desktopTest, androidTest, and/or iosTest. + */ @OptIn(ExperimentalCoroutinesApi::class) class MastodonApiTests { - // TODO: fix loading json from resources + @Test - @Ignore fun `Instance request should fail with invalid response`() = runTest { // given - // val content: String = javaClass.classLoader.getResource("response_instance_invalid.json").readText() - val content: String = "" + + // given + val content = MR.files.response_account_required.readText() val mastodonApi = MastodonApiImpl( httpClient = createMockClient( - statusCode = HttpStatusCode.Unauthorized, content = ByteReadChannel(text = content) + statusCode = HttpStatusCode.Unauthorized, + content = ByteReadChannel(text = content) ) ) @@ -52,14 +48,10 @@ class MastodonApiTests { assertNull(actual = result.getOrNull()?.uri) } - // TODO: fix loading json from resources @Test - @Ignore fun `Instance request should succeed with required field response`() = runTest { // given - // val content: String = javaClass.classLoader.getResource("response_instance_valid.json").readText() - val content: String = "" - + val content = MR.files.response_instance_valid.readText() val mastodonApi = MastodonApiImpl( httpClient = createMockClient( statusCode = HttpStatusCode.Unauthorized, content = ByteReadChannel(text = content) @@ -72,6 +64,7 @@ class MastodonApiTests { // then assertTrue(actual = result.isSuccess) assertNotNull(actual = result.getOrNull()?.uri) + } private fun createMockClient( @@ -97,4 +90,5 @@ class MastodonApiTests { } } } + } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1fb1dffc..0eb52de3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,7 @@ androidx-appcompat = "1.5.1" androidx-activity = "1.6.1" androidx-compose-foundation = "1.2.1" com-arkivanov-decompose = "1.0.0-alpha-05" +dev-icerock-moko-resources = "0.20.1" [libraries] @@ -23,6 +24,7 @@ org-jetbrains-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gra com-android-tools-build-gradle = { module = "com.android.tools.build:gradle", version.ref = "com-android-tools-build" } org-jetbrains-kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "org-jetbrains-kotlin" } com-squareup-sqldelight-gradle-plugin = { module = "com.squareup.sqldelight:gradle-plugin", version.ref = "com-squareup-sqldelight" } +dev-icerock-moko-resources-generator = { module = "dev.icerock.moko:resources-generator", version.ref = "dev-icerock-moko-resources"} # libraries io-ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "io-ktor" } @@ -41,6 +43,10 @@ org-jetbrains-kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-j org-jetbrains-kotlin-test-common = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "org-jetbrains-kotlin" } org-jetbrains-kotlin-test-annotations-common = { module = "org.jetbrains.kotlin:kotlin-test-annotations-common", version.ref = "org-jetbrains-kotlin" } +dev-icerock-moko-resources-common = { module = "dev.icerock.moko:resources", version.ref = "dev-icerock-moko-resources"} +dev-icerock-moko-resources-jvm = { module = "dev.icerock.moko:resources-compose", version.ref = "dev-icerock-moko-resources"} +dev-icerock-moko-resources-test = { module = "dev.icerock.moko:resources-test", version.ref = "dev-icerock-moko-resources"} + com-squareup-sqldelight-android-driver = { module = "com.squareup.sqldelight:android-driver", version.ref = "com-squareup-sqldelight" } com-squareup-sqldelight-native-driver = { module = "com.squareup.sqldelight:native-driver", version.ref = "com-squareup-sqldelight" } com-squareup-sqldelight-sqlite-driver = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "com-squareup-sqldelight" } diff --git a/ui/common/build.gradle.kts b/ui/common/build.gradle.kts index 98cec28c..f3322279 100644 --- a/ui/common/build.gradle.kts +++ b/ui/common/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("org.jetbrains.compose") id("com.android.library") id("com.diffplug.spotless") version "6.11.0" + id("dev.icerock.mobile.multiplatform-resources") } spotless { kotlin { @@ -51,6 +52,7 @@ kotlin { implementation(compose.material) implementation(libs.com.arkivanov.decompose) implementation(libs.com.arkivanov.decompose.extensions.compose.jetbrains) + implementation(libs.dev.icerock.moko.resources.common) } } @@ -58,12 +60,23 @@ kotlin { dependencies { // Workaround for https://github.com/JetBrains/compose-jb/issues/2340 implementation(libs.androidx.compose.foundation) + implementation(libs.dev.icerock.moko.resources.jvm) } } named("desktopMain") { dependencies { implementation(compose.desktop.common) + implementation(libs.dev.icerock.moko.resources.jvm) + } + } + + //Testing + named("commonTest") { + dependencies { + implementation(kotlin("test")) + implementation(libs.org.jetbrains.kotlin.test.common) + implementation(libs.org.jetbrains.kotlin.test.annotations.common) } } } @@ -72,3 +85,11 @@ kotlin { kotlinOptions.jvmTarget = "11" } } + +multiplatformResources { + multiplatformResourcesPackage = "social.androiddev.common" // required +// multiplatformResourcesClassName = "SharedRes" // optional, default MR +// multiplatformResourcesVisibility = dev.icerock.gradle.MRVisibility.Public // optional, default Public +// iosBaseLocalizationRegion = "en" // optional, default "en" +// multiplatformResourcesSourceSet = "commonMain" // optional, default "commonMain" +} \ No newline at end of file diff --git a/ui/common/src/commonMain/kotlin/social/androiddev/common/sharedres/SharedResources.kt b/ui/common/src/commonMain/kotlin/social/androiddev/common/sharedres/SharedResources.kt new file mode 100644 index 00000000..745486b0 --- /dev/null +++ b/ui/common/src/commonMain/kotlin/social/androiddev/common/sharedres/SharedResources.kt @@ -0,0 +1,46 @@ +package social.androiddev.common.sharedres + +import dev.icerock.moko.graphics.Color +import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.desc.Resource +import dev.icerock.moko.resources.desc.StringDesc +import dev.icerock.moko.resources.getImageByFileName +import social.androiddev.common.MR + +object SharedResources { + + /** + * A String that can be localised. + * On Android, call `toString(context)`, on iOS, call `localized()` + * + * @see https://github.com/icerockdev/moko-resources#example-1---simple-localization-string + */ + fun getLocalisedHello(): StringDesc { + + return StringDesc.Resource(MR.strings.my_string) + } + + /** + * Get an image resource by the file name, and falls back to mastodon logo if it fails + * + * @see https://github.com/icerockdev/moko-resources#example-7---pass-image + */ + fun getImageByFileName(name: String): ImageResource { + val fallbackImage = MR.images.mastodon_logo + return MR.images.getImageByFileName(name) ?: fallbackImage + } + + /** + * Android: val color: Color = MR.colors.valueColor.getColor(context = this) + * + * iOS:val color: UIColor = MR.colors.valueColor.getColor(UIScreen.main.traitCollection.userInterfaceStyle) + * + * @see https://github.com/icerockdev/moko-resources#example-9---pass-colors + */ + fun getValueColor(): Color { + return MR.colors.valueColor.color + } + + + +} \ No newline at end of file diff --git a/ui/common/src/commonMain/resources/MR/base/strings.xml b/ui/common/src/commonMain/resources/MR/base/strings.xml new file mode 100644 index 00000000..74d2165c --- /dev/null +++ b/ui/common/src/commonMain/resources/MR/base/strings.xml @@ -0,0 +1,4 @@ + + + My default localization string + \ No newline at end of file diff --git a/ui/common/src/commonMain/resources/MR/colors/colors.xml b/ui/common/src/commonMain/resources/MR/colors/colors.xml new file mode 100644 index 00000000..cc914249 --- /dev/null +++ b/ui/common/src/commonMain/resources/MR/colors/colors.xml @@ -0,0 +1,14 @@ + + + + #B02743FF + @color/valueColor + + 0xB92743FF + 7CCFEEFF + + + @color/valueColor + @color/referenceColor + + \ No newline at end of file diff --git a/ui/common/src/commonMain/resources/MR/images/mastodon_logo@1.5x.png b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@1.5x.png new file mode 100644 index 00000000..b6fe79b1 Binary files /dev/null and b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@1.5x.png differ diff --git a/ui/common/src/commonMain/resources/MR/images/mastodon_logo@1x.png b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@1x.png new file mode 100644 index 00000000..d5651c72 Binary files /dev/null and b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@1x.png differ diff --git a/ui/common/src/commonMain/resources/MR/images/mastodon_logo@2x.png b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@2x.png new file mode 100644 index 00000000..b6af9e9f Binary files /dev/null and b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@2x.png differ diff --git a/ui/common/src/commonMain/resources/MR/images/mastodon_logo@3x.png b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@3x.png new file mode 100644 index 00000000..d29cd0dc Binary files /dev/null and b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@3x.png differ diff --git a/ui/common/src/commonMain/resources/MR/images/mastodon_logo@4x.png b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@4x.png new file mode 100644 index 00000000..bb6dd7de Binary files /dev/null and b/ui/common/src/commonMain/resources/MR/images/mastodon_logo@4x.png differ diff --git a/ui/common/src/commonMain/resources/MR/ru/strings.xml b/ui/common/src/commonMain/resources/MR/ru/strings.xml new file mode 100644 index 00000000..bac38a5a --- /dev/null +++ b/ui/common/src/commonMain/resources/MR/ru/strings.xml @@ -0,0 +1,4 @@ + + + Моя строка локализации по умолчанию + \ No newline at end of file diff --git a/ui/common/src/commonTest/kotlin/social/androiddev/common/sharedres/SharedResourcesTest.kt b/ui/common/src/commonTest/kotlin/social/androiddev/common/sharedres/SharedResourcesTest.kt new file mode 100644 index 00000000..eb6bedba --- /dev/null +++ b/ui/common/src/commonTest/kotlin/social/androiddev/common/sharedres/SharedResourcesTest.kt @@ -0,0 +1,38 @@ +package social.androiddev.common.sharedres + +import dev.icerock.moko.graphics.Color +import kotlin.test.assertEquals +import kotlin.test.Test + +/** + * These tests aren't ideal, but they show that there's some file access going on + */ +class SharedResourcesTest { + + @Test + fun `getLocalisedHello returns a stringresource with unknown content`() { + + // This test only confirms that the resource is there. To get the content, it must run on androidTest, desktopTest + assertEquals( + "ResourceStringDesc(stringRes=StringResource(resourceId=2132017188))", + SharedResources.getLocalisedHello().toString() + ) + + } + + @Test + fun `getting empty image still returns a fallback image`() { + assertEquals( + "dev.icerock.moko.resources.ImageResource@821330f", + SharedResources.getImageByFileName("").toString() + ) + } + + @Test + fun `getValueColor returns a specific Color class`() { + assertEquals( + Color(red=176, green=39, blue=67, alpha=255), + SharedResources.getValueColor() + ) + } +} \ No newline at end of file