├── uitextcompose-android-sample
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── resources.properties
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ │ ├── themes.xml
│ │ │ ├── colors.xml
│ │ │ └── strings.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values-ro
│ │ │ └── strings.xml
│ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ └── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ └── com
│ │ │ └── radusalagean
│ │ │ └── uitextcompose
│ │ │ └── android
│ │ │ └── sample
│ │ │ ├── ui
│ │ │ ├── component
│ │ │ │ ├── ExampleEntryModel.kt
│ │ │ │ ├── LanguageOption.kt
│ │ │ │ ├── SectionHeader.kt
│ │ │ │ ├── Section.kt
│ │ │ │ └── ExampleEntry.kt
│ │ │ ├── theme
│ │ │ │ └── Color.kt
│ │ │ └── screen
│ │ │ │ ├── MainScreen.kt
│ │ │ │ └── MainViewModel.kt
│ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── examples
├── raw.png
├── res.png
├── pluralRes.png
├── res_annotated.png
├── compound_example_1.png
├── compound_example_2.png
├── compound_example_3.png
├── pluralRes_annotated.png
└── terms_of_service_and_privacy_policy.png
├── ui-text-compose.png
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── uitextcompose-multiplatform-sample
├── iosApp
│ ├── iosApp
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── AppIcon.appiconset
│ │ │ │ ├── app-icon-1024.png
│ │ │ │ └── Contents.json
│ │ │ └── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ ├── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ │ └── Contents.json
│ │ ├── iOSApp.swift
│ │ ├── ContentView.swift
│ │ └── Info.plist
│ ├── Configuration
│ │ └── Config.xcconfig
│ └── .gitignore
├── src
│ ├── wasmJsMain
│ │ ├── resources
│ │ │ ├── styles.css
│ │ │ └── index.html
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── radusalagean
│ │ │ └── uitextcompose
│ │ │ └── multiplatform
│ │ │ └── sample
│ │ │ ├── di
│ │ │ └── PlatformModule.wasmJs.kt
│ │ │ ├── util
│ │ │ └── LanguageManagerWasmJs.kt
│ │ │ └── main.wasmJs.kt
│ ├── androidMain
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── xml
│ │ │ │ └── locales_config.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── radusalagean
│ │ │ │ └── uitextcompose
│ │ │ │ └── multiplatform
│ │ │ │ └── sample
│ │ │ │ ├── AndroidApplication.kt
│ │ │ │ ├── di
│ │ │ │ └── PlatformModule.android.kt
│ │ │ │ ├── MainActivity.kt
│ │ │ │ └── util
│ │ │ │ └── LanguageManagerAndroid.kt
│ │ └── AndroidManifest.xml
│ ├── commonMain
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── radusalagean
│ │ │ │ └── uitextcompose
│ │ │ │ └── multiplatform
│ │ │ │ └── sample
│ │ │ │ ├── di
│ │ │ │ ├── AppModule.kt
│ │ │ │ ├── PlatformModule.kt
│ │ │ │ └── CommonModule.kt
│ │ │ │ ├── util
│ │ │ │ └── LanguageManager.kt
│ │ │ │ ├── ui
│ │ │ │ ├── component
│ │ │ │ │ ├── ExampleEntryModel.kt
│ │ │ │ │ ├── LanguageOption.kt
│ │ │ │ │ ├── SectionHeader.kt
│ │ │ │ │ ├── Section.kt
│ │ │ │ │ └── ExampleEntry.kt
│ │ │ │ ├── theme
│ │ │ │ │ └── Color.kt
│ │ │ │ └── screen
│ │ │ │ │ ├── MainScreen.kt
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ └── main.common.kt
│ │ └── composeResources
│ │ │ ├── values-ro
│ │ │ └── strings.xml
│ │ │ └── values
│ │ │ └── strings.xml
│ ├── iosMain
│ │ └── kotlin
│ │ │ └── com
│ │ │ └── radusalagean
│ │ │ └── uitextcompose
│ │ │ └── multiplatform
│ │ │ └── sample
│ │ │ ├── MainViewController.kt
│ │ │ ├── di
│ │ │ └── PlatformModule.ios.kt
│ │ │ └── util
│ │ │ └── LanguageManagerIOS.kt
│ └── desktopMain
│ │ └── kotlin
│ │ └── com
│ │ └── radusalagean
│ │ └── uitextcompose
│ │ └── multiplatform
│ │ └── sample
│ │ ├── di
│ │ └── PlatformModule.desktop.kt
│ │ ├── util
│ │ └── LanguageManagerDesktop.kt
│ │ └── main.desktop.kt
└── build.gradle.kts
├── .idea
├── misc.xml
├── runConfigurations
│ ├── multiplatform_sample_ios.xml
│ ├── multiplatform_sample_wasmJs.xml
│ ├── multiplatform_sample_desktop.xml
│ ├── android-sample.xml
│ └── multiplatform_sample_android.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── uitextcompose-core
├── src
│ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── radusalagean
│ │ └── uitextcompose
│ │ └── core
│ │ ├── InternalApi.kt
│ │ ├── UITextBase.kt
│ │ ├── UITextAnnotation.kt
│ │ ├── UITextBuilderBase.kt
│ │ └── UITextUtil.kt
├── build.gradle.kts
└── api
│ ├── android
│ └── uitextcompose-core.api
│ └── desktop
│ └── uitextcompose-core.api
├── settings.gradle.kts
├── .github
└── workflows
│ ├── publish.yml
│ └── gradle.yml
├── .gitignore
├── gradle.properties
├── uitextcompose-android
├── src
│ ├── androidInstrumentedTest
│ │ └── res
│ │ │ └── values
│ │ │ └── strings.xml
│ └── androidMain
│ │ └── kotlin
│ │ └── com
│ │ └── radusalagean
│ │ └── uitextcompose
│ │ └── android
│ │ ├── UITextBuilder.kt
│ │ └── UIText.kt
├── api
│ └── uitextcompose-android.api
└── build.gradle.kts
├── uitextcompose-multiplatform
├── src
│ ├── desktopTest
│ │ └── composeResources
│ │ │ └── values
│ │ │ └── strings.xml
│ └── commonMain
│ │ └── kotlin
│ │ └── com
│ │ └── radusalagean
│ │ └── uitextcompose
│ │ └── multiplatform
│ │ ├── UITextBuilder.kt
│ │ └── UIText.kt
├── build.gradle.kts
└── api
│ ├── android
│ └── uitextcompose-multiplatform.api
│ └── desktop
│ └── uitextcompose-multiplatform.api
├── gradlew.bat
└── gradlew
/uitextcompose-android-sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/resources.properties:
--------------------------------------------------------------------------------
1 | unqualifiedResLocale=en
--------------------------------------------------------------------------------
/examples/raw.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/raw.png
--------------------------------------------------------------------------------
/examples/res.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/res.png
--------------------------------------------------------------------------------
/ui-text-compose.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/ui-text-compose.png
--------------------------------------------------------------------------------
/examples/pluralRes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/pluralRes.png
--------------------------------------------------------------------------------
/examples/res_annotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/res_annotated.png
--------------------------------------------------------------------------------
/examples/compound_example_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/compound_example_1.png
--------------------------------------------------------------------------------
/examples/compound_example_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/compound_example_2.png
--------------------------------------------------------------------------------
/examples/compound_example_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/compound_example_3.png
--------------------------------------------------------------------------------
/examples/pluralRes_annotated.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/pluralRes_annotated.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/examples/terms_of_service_and_privacy_policy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/examples/terms_of_service_and_privacy_policy.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/wasmJsMain/resources/styles.css:
--------------------------------------------------------------------------------
1 | html, body {
2 | width: 100%;
3 | height: 100%;
4 | margin: 0;
5 | padding: 0;
6 | overflow: hidden;
7 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=com.radusalagean.uitextcompose.sample.UITextComposeSample
3 | APP_NAME=UIText Compose Multiplatform Sample
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-android-sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.di
2 |
3 | fun appModule() = listOf(commonModule, platformModule)
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/radusalagean/ui-text-compose/HEAD/uitextcompose-multiplatform-sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/di/PlatformModule.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.di
2 |
3 | import org.koin.core.module.Module
4 |
5 | expect val platformModule: Module
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
4 | networkTimeout=10000
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/xml/locales_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/util/LanguageManager.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.util
2 |
3 | interface LanguageManager {
4 | fun getCurrentLanguageCode(): String
5 | fun onLanguageSelected(code: String)
6 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import shared
3 |
4 | @main
5 | struct iOSApp: App {
6 | init() {
7 | Main_commonKt.application()
8 | }
9 | var body: some Scene {
10 | WindowGroup {
11 | ContentView()
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/component/ExampleEntryModel.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.component
2 |
3 | import com.radusalagean.uitextcompose.android.UIText
4 |
5 | data class ExampleEntryModel(
6 | val label: String,
7 | val value: UIText
8 | )
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/component/LanguageOption.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.component
2 |
3 | import com.radusalagean.uitextcompose.android.UIText
4 |
5 | data class LanguageOption(
6 | val uiText: UIText,
7 | val languageCode: String
8 | )
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/component/ExampleEntryModel.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.component
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.UIText
4 |
5 | data class ExampleEntryModel(
6 | val label: String,
7 | val value: UIText
8 | )
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/component/LanguageOption.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.component
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.UIText
4 |
5 | data class LanguageOption(
6 | val uiText: UIText,
7 | val languageCode: String
8 | )
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/AndroidApplication.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample
2 |
3 | import android.app.Application
4 |
5 | class AndroidApplication : Application() {
6 |
7 | override fun onCreate() {
8 | super.onCreate()
9 | application()
10 | }
11 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/iosMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/MainViewController.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample
2 |
3 | import androidx.compose.ui.window.ComposeUIViewController
4 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.screen.MainScreen
5 |
6 | fun MainViewController() = ComposeUIViewController { MainScreen() }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/di/CommonModule.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.di
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.screen.MainViewModel
4 | import org.koin.core.module.dsl.viewModelOf
5 | import org.koin.dsl.module
6 |
7 | val commonModule = module {
8 | viewModelOf(::MainViewModel)
9 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/iosMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/di/PlatformModule.ios.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.di
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManager
4 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManagerIOS
5 | import org.koin.dsl.module
6 |
7 | actual val platformModule = module {
8 | single { LanguageManagerIOS() }
9 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 |
13 | val CustomGreen = Color(0xFF56C025)
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | UIText Compose Multiplatform Sample
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/wasmJsMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/di/PlatformModule.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.di
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManager
4 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManagerWasmJs
5 | import org.koin.dsl.module
6 |
7 | actual val platformModule = module {
8 | single { LanguageManagerWasmJs() }
9 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/di/PlatformModule.android.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.di
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManager
4 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManagerAndroid
5 | import org.koin.dsl.module
6 |
7 | actual val platformModule = module {
8 | single { LanguageManagerAndroid() }
9 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/desktopMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/di/PlatformModule.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.di
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManager
4 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManagerDesktop
5 | import org.koin.dsl.module
6 |
7 | actual val platformModule = module {
8 | single { LanguageManagerDesktop() }
9 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/main.common.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample
2 |
3 | import com.radusalagean.uitextcompose.multiplatform.sample.di.appModule
4 | import io.github.aakira.napier.DebugAntilog
5 | import io.github.aakira.napier.Napier
6 | import org.koin.core.context.startKoin
7 |
8 | fun application() {
9 | Napier.base(DebugAntilog())
10 | startKoin {
11 | modules(appModule())
12 | }
13 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 |
13 | val CustomGreen = Color(0xFF56C025)
--------------------------------------------------------------------------------
/uitextcompose-core/src/commonMain/kotlin/com/radusalagean/uitextcompose/core/InternalApi.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.core
2 |
3 | @RequiresOptIn(
4 | level = RequiresOptIn.Level.ERROR,
5 | message = "This is an internal API and should not be used directly. It is intended for use only by other uitextcompose modules."
6 | )
7 | @Retention(AnnotationRetention.BINARY)
8 | @Target(
9 | AnnotationTarget.CLASS,
10 | AnnotationTarget.FUNCTION,
11 | AnnotationTarget.PROPERTY
12 | )
13 | public annotation class InternalApi
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "ui-text-compose"
2 |
3 | pluginManagement {
4 | repositories {
5 | google()
6 | mavenCentral()
7 | gradlePluginPortal()
8 | }
9 | }
10 |
11 | dependencyResolutionManagement {
12 | repositories {
13 | google()
14 | mavenCentral()
15 | }
16 | }
17 |
18 | include(":uitextcompose-core")
19 | include(":uitextcompose-multiplatform")
20 | include(":uitextcompose-multiplatform-sample")
21 | include(":uitextcompose-android")
22 | include(":uitextcompose-android-sample")
23 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/multiplatform_sample_ios.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/wasmJsMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/util/LanguageManagerWasmJs.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.util
2 |
3 | import kotlinx.browser.window
4 |
5 | class LanguageManagerWasmJs : LanguageManager {
6 | override fun getCurrentLanguageCode(): String {
7 | return getBrowserLanguage()
8 | }
9 |
10 | override fun onLanguageSelected(code: String) {
11 | // No-op
12 | }
13 |
14 | private fun getBrowserLanguage(): String {
15 | return window.navigator.language.substringBefore("-")
16 | }
17 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import shared
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | MainViewControllerKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
17 | }
18 | }
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample
2 |
3 | import android.os.Bundle
4 | import androidx.activity.compose.setContent
5 | import androidx.appcompat.app.AppCompatActivity
6 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.screen.MainScreen
7 |
8 | class MainActivity : AppCompatActivity() {
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContent {
13 | MainScreen()
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/wasmJsMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/main.wasmJs.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.window.ComposeViewport
5 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.screen.MainScreen
6 | import kotlinx.browser.document
7 |
8 | @OptIn(ExperimentalComposeUiApi::class)
9 | fun main() {
10 | application()
11 |
12 | ComposeViewport(document.body!!) {
13 | MainScreen(
14 | languagePickerEnabled = false
15 | )
16 | }
17 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/component/SectionHeader.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.component
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import com.radusalagean.uitextcompose.android.UIText
8 |
9 | @Composable
10 | fun SectionHeader(
11 | text: UIText,
12 | modifier: Modifier = Modifier
13 | ) {
14 | Text(
15 | text = text.buildAnnotatedStringComposable(),
16 | modifier = modifier,
17 | style = MaterialTheme.typography.titleLarge
18 | )
19 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/component/SectionHeader.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.component
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import com.radusalagean.uitextcompose.multiplatform.UIText
8 |
9 | @Composable
10 | fun SectionHeader(
11 | text: UIText,
12 | modifier: Modifier = Modifier
13 | ) {
14 | Text(
15 | text = text.buildStringComposable(),
16 | modifier = modifier,
17 | style = MaterialTheme.typography.titleLarge
18 | )
19 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/desktopMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/util/LanguageManagerDesktop.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.util
2 |
3 | import androidx.compose.runtime.getValue
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.runtime.setValue
6 | import java.util.Locale
7 |
8 | class LanguageManagerDesktop : LanguageManager {
9 |
10 | var currentLanguage: String by mutableStateOf(Locale.getDefault().language)
11 | private set
12 |
13 | override fun getCurrentLanguageCode(): String {
14 | return currentLanguage
15 | }
16 |
17 | override fun onLanguageSelected(code: String) {
18 | Locale.setDefault(Locale.of(code))
19 | currentLanguage = code
20 | }
21 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/component/Section.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.component
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.unit.dp
8 | import com.radusalagean.uitextcompose.android.UIText
9 |
10 | @Composable
11 | fun Section(
12 | title: UIText,
13 | modifier: Modifier = Modifier,
14 | content: @Composable () -> Unit
15 | ) {
16 | Column(
17 | modifier = modifier,
18 | verticalArrangement = Arrangement.spacedBy(16.dp)
19 | ) {
20 | SectionHeader(
21 | text = title
22 | )
23 | content()
24 | }
25 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/util/LanguageManagerAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.util
2 |
3 | import androidx.appcompat.app.AppCompatDelegate
4 | import androidx.core.os.LocaleListCompat
5 |
6 | class LanguageManagerAndroid : LanguageManager {
7 | override fun getCurrentLanguageCode(): String {
8 | return extractLanguageCode()
9 | }
10 |
11 | override fun onLanguageSelected(code: String) {
12 | val localesList = LocaleListCompat.forLanguageTags(code)
13 | AppCompatDelegate.setApplicationLocales(localesList)
14 | }
15 |
16 | private fun extractLanguageCode(): String {
17 | val locales = AppCompatDelegate.getApplicationLocales()
18 | return locales.get(0)?.language ?: "en"
19 | }
20 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample
2 |
3 | import android.os.Bundle
4 | import androidx.activity.compose.setContent
5 | import androidx.activity.enableEdgeToEdge
6 | import androidx.activity.viewModels
7 | import androidx.appcompat.app.AppCompatActivity
8 | import com.radusalagean.uitextcompose.android.sample.ui.screen.MainScreen
9 | import com.radusalagean.uitextcompose.android.sample.ui.screen.MainViewModel
10 |
11 | class MainActivity : AppCompatActivity() {
12 |
13 | private val viewModel: MainViewModel by viewModels()
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | enableEdgeToEdge()
18 | setContent {
19 | MainScreen(viewModel)
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/component/Section.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.component
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.unit.dp
8 | import com.radusalagean.uitextcompose.multiplatform.UIText
9 |
10 | @Composable
11 | fun Section(
12 | title: UIText,
13 | modifier: Modifier = Modifier,
14 | content: @Composable () -> Unit
15 | ) {
16 | Column(
17 | modifier = modifier,
18 | verticalArrangement = Arrangement.spacedBy(16.dp)
19 | ) {
20 | SectionHeader(
21 | text = title
22 | )
23 | content()
24 | }
25 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | .DS_Store
3 | build/
4 | DerivedData/
5 | xcuserdata/
6 | *.xcuserstate
7 |
8 | # SwiftPM
9 | .swiftpm/
10 | .build/
11 |
12 | # CocoaPods
13 | Pods/
14 | Podfile.lock
15 |
16 | # Carthage
17 | Carthage/Build/
18 |
19 | # Xcode specific
20 | *.xcodeproj/*
21 | !*.xcodeproj/project.pbxproj
22 | !*.xcodeproj/xcshareddata/
23 | !*.xcodeproj/xcshareddata/WorkspaceSettings.xcsettings
24 |
25 | *.xcworkspace/*
26 | !*.xcworkspace/contents.xcworkspacedata
27 |
28 | # Fastlane
29 | fastlane/report.xml
30 | fastlane/Preview.html
31 | fastlane/screenshots
32 | fastlane/test_output
33 |
34 | # Archives
35 | *.xcarchive
36 |
37 | # App data
38 | *.ipa
39 | *.dSYM.zip
40 | *.dSYM
41 |
42 | # Provisioning profiles
43 | *.mobileprovision
44 |
45 | # Symbolication
46 | *.symbolication
47 |
48 | # User-specific
49 | *.swp
50 | *.lock
51 | *.orig
52 | *.log
53 |
54 | # Swift Package Manager checkouts
55 | .checkouts/
56 |
57 | # Misc
58 | *.orig
59 | *.rej
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | publish:
7 | name: Release build and publish
8 | runs-on: macOS-latest
9 | steps:
10 | - name: Check out code
11 | uses: actions/checkout@v4
12 | - name: Set up JDK 21
13 | uses: actions/setup-java@v4
14 | with:
15 | distribution: 'zulu'
16 | java-version: 21
17 | - name: Publish to MavenCentral
18 | run: ./gradlew publishToMavenCentral --no-configuration-cache
19 | env:
20 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
21 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
22 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }}
23 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
24 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY_CONTENTS }}
25 |
26 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/desktopMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/main.desktop.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample
2 |
3 | import androidx.compose.runtime.key
4 | import androidx.compose.ui.window.Window
5 | import androidx.compose.ui.window.application
6 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.screen.MainScreen
7 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManager
8 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManagerDesktop
9 | import org.koin.compose.koinInject
10 |
11 | fun main() = application {
12 | application()
13 | val languageManager: LanguageManagerDesktop =
14 | koinInject() as LanguageManagerDesktop
15 |
16 | Window(
17 | onCloseRequest = ::exitApplication,
18 | title = "UIText Compose Multiplatform Sample",
19 | ) {
20 | key(languageManager.currentLanguage) {
21 | MainScreen()
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # === Android Studio & IntelliJ ===
2 | .idea/*
3 | !.idea/codeStyles
4 | !.idea/inspectionProfiles
5 | !.idea/misc.xml
6 | !.idea/modules.xml
7 | !.idea/runConfigurations
8 | !.idea/.name
9 |
10 | # === Project files ===
11 | *.iml
12 | .gradle
13 | /local.properties
14 | /.gradle
15 | /captures
16 | /build
17 | /buildSrc/build/
18 | **/build/
19 | .cxx/
20 | output.json
21 |
22 | # === Android Profiling ===
23 | *.hprof
24 |
25 | # === OS-specific files ===
26 | .DS_Store
27 | Thumbs.db
28 |
29 | # === Log files ===
30 | *.log
31 |
32 | # === Keystore files (if present) ===
33 | *.jks
34 | *.keystore
35 |
36 | # === Crashlytics/Fabric ===
37 | crashlytics.properties
38 | crashlytics-build.properties
39 | fabric.properties
40 |
41 | # === NDK / CMake / External Native Build ===
42 | .externalNativeBuild
43 |
44 | # === Others ===
45 | /outputs/
46 | *.apk
47 | *.ap_
48 | *.aab
49 |
50 | # === Kotlin/Jetpack Compose build cache ===
51 | /.kotlinc/
52 | .kotlin
53 |
54 | # === Google Services (e.g. APIs or Firebase) ===
55 | google-services.json
56 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/iosMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/util/LanguageManagerIOS.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.util
2 |
3 | import platform.Foundation.NSLocale
4 | import platform.Foundation.NSURL
5 | import platform.Foundation.currentLocale
6 | import platform.Foundation.languageCode
7 | import platform.UIKit.UIApplication
8 | import platform.UIKit.UIApplicationOpenSettingsURLString
9 |
10 | class LanguageManagerIOS : LanguageManager {
11 | override fun getCurrentLanguageCode(): String {
12 | return NSLocale.currentLocale.languageCode()
13 | }
14 |
15 | override fun onLanguageSelected(code: String) {
16 | val url = NSURL(string = UIApplicationOpenSettingsURLString)
17 | val application = UIApplication.sharedApplication
18 | if (application.canOpenURL(url)) {
19 | application.openURL(
20 | url,
21 | options = emptyMap(),
22 | completionHandler = null
23 | )
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/.idea/runConfigurations/multiplatform_sample_wasmJs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | UIText Compose Android Sample
3 |
4 | Language
5 | English
6 | Romanian
7 |
8 | Examples
9 | Hi, %1$s!
10 | You have %1$s in your %2$s.
11 | shopping cart
12 | Proceed to checkout
13 |
14 | This is how you can create a %1$s and %2$s text with links.
15 | Terms of Service
16 | Privacy Policy
17 |
18 |
19 | - %1$s product
20 | - %1$s products
21 |
22 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/values-ro/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Limba
4 | engleză
5 | română
6 |
7 | Exemple
8 | Salut, %1$s!
9 | Ai %1$s în %2$s.
10 | coșul tău de cumpăraturi
11 | Finalizează comanda
12 |
13 | Așa poți crea un text cu link-uri pentru %1$s și %2$s.
14 | Termeni și condiții
15 | Politica de Confidențialitate
16 |
17 |
18 | - %1$s produs
19 | - %1$s produse
20 | - %1$s de produse
21 |
22 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/composeResources/values-ro/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Limba
4 | engleză
5 | română
6 |
7 | Exemple
8 | Salut, %1$s!
9 | Ai %1$s în %2$s.
10 | coșul tău de cumpăraturi
11 | Finalizează comanda
12 |
13 | Așa poți crea un text cu link-uri pentru %1$s și %2$s.
14 | Termeni și condiții
15 | Politica de Confidențialitate
16 |
17 |
18 | - %1$s produs
19 | - %1$s produse
20 | - %1$s de produse
21 |
22 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Gradle
2 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M"
3 | org.gradle.caching=true
4 | org.gradle.configuration-cache=true
5 | #Kotlin
6 | kotlin.code.style=official
7 | #MPP
8 | kotlin.mpp.enableCInteropCommonization=true
9 | #Android
10 | android.useAndroidX=true
11 | android.nonTransitiveRClass=true
12 |
13 | POM_NAME=UIText Compose
14 | POM_DESCRIPTION=A Kotlin Multiplatform library for creating text blueprints in Compose applications, supporting both plain and styled text with String Resources integration.
15 | POM_INCEPTION_YEAR=2025
16 | POM_URL=https://github.com/radusalagean/ui-text-compose
17 |
18 | POM_LICENSE_NAME=The Apache Software License, Version 2.0
19 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
20 | POM_LICENSE_DIST=repo
21 |
22 | POM_SCM_URL=https://github.com/radusalagean/ui-text-compose
23 | POM_SCM_CONNECTION=scm:git:git://github.com/radusalagean/ui-text-compose.git
24 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/radusalagean/ui-text-compose.git
25 |
26 | POM_DEVELOPER_ID=radusalagean
27 | POM_DEVELOPER_NAME=Radu Salagean
28 | POM_DEVELOPER_URL=https://github.com/radusalagean
29 | POM_DEVELOPER_EMAIL=contact@radusalagean.com
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/composeResources/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | UIText Compose Multiplatform Sample
4 |
5 | Language
6 | English
7 | Romanian
8 |
9 | Examples
10 | Hi, %1$s!
11 | You have %1$s in your %2$s.
12 | shopping cart
13 | Proceed to checkout
14 |
15 | This is how you can create a %1$s and %2$s text with links.
16 | Terms of Service
17 | Privacy Policy
18 |
19 |
20 | - %1$s product
21 | - %1$s products
22 |
23 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/multiplatform_sample_desktop.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | true
24 | true
25 | false
26 | false
27 |
28 |
29 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/component/ExampleEntry.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.component
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.unit.dp
12 | import com.radusalagean.uitextcompose.android.sample.ui.theme.PurpleGrey40
13 |
14 | @Composable
15 | fun ExampleEntry(
16 | model: ExampleEntryModel,
17 | modifier: Modifier = Modifier
18 | ) {
19 | Column(modifier = modifier) {
20 | Text(
21 | text = model.label,
22 | style = MaterialTheme.typography.labelSmall.copy(color = Color.White),
23 | modifier = Modifier
24 | .background(color = PurpleGrey40)
25 | .padding(horizontal = 4.dp)
26 | )
27 | Text(
28 | text = model.value.buildAnnotatedStringComposable(),
29 | style = MaterialTheme.typography.titleMedium
30 | )
31 | }
32 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/component/ExampleEntry.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.component
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.unit.dp
12 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.theme.PurpleGrey40
13 |
14 | @Composable
15 | fun ExampleEntry(
16 | model: ExampleEntryModel,
17 | modifier: Modifier = Modifier
18 | ) {
19 | Column(modifier = modifier) {
20 | Text(
21 | text = model.label,
22 | style = MaterialTheme.typography.labelSmall.copy(color = Color.White),
23 | modifier = Modifier
24 | .background(color = PurpleGrey40)
25 | .padding(horizontal = 4.dp)
26 | )
27 | Text(
28 | text = model.value.buildAnnotatedStringComposable(),
29 | style = MaterialTheme.typography.titleMedium
30 | )
31 | }
32 | }
--------------------------------------------------------------------------------
/uitextcompose-core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.radusalagean.uitextcompose.Config
2 | import com.vanniktech.maven.publish.SonatypeHost
3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
4 |
5 | plugins {
6 | alias(libs.plugins.kotlin.multiplatform)
7 | alias(libs.plugins.kotlin.binaryCompatibilityValidator)
8 | alias(libs.plugins.android.library)
9 | alias(libs.plugins.compose.compiler)
10 | alias(libs.plugins.compose.multiplatform)
11 | alias(libs.plugins.vanniktech.mavenPublish)
12 | }
13 |
14 | group = Config.artifactGroup
15 | version = Config.versionName
16 |
17 | kotlin {
18 | jvm("desktop")
19 | androidTarget {
20 | publishLibraryVariants("release")
21 | }
22 | iosX64()
23 | iosArm64()
24 | iosSimulatorArm64()
25 | @OptIn(ExperimentalWasmDsl::class)
26 | wasmJs {
27 | browser()
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | api(compose.runtime)
34 | api(compose.ui)
35 | }
36 | }
37 | }
38 | explicitApi()
39 | }
40 |
41 | android {
42 | namespace = "${Config.rootPackage}.core"
43 | compileSdk = Config.compileSdk
44 | defaultConfig {
45 | minSdk = Config.minSdk
46 | }
47 | compileOptions {
48 | sourceCompatibility = JavaVersion.VERSION_11
49 | targetCompatibility = JavaVersion.VERSION_11
50 | }
51 | }
52 |
53 | mavenPublishing {
54 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
55 |
56 | signAllPublications()
57 |
58 | coordinates(group.toString(), "ui-text-compose-core", version.toString())
59 | }
60 |
--------------------------------------------------------------------------------
/uitextcompose-android/src/androidInstrumentedTest/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Instrumented test string
4 | Hello from %1$s test!
5 | %1$s has %2$s %3$s
6 | 1: %s 2: %1$s 3: %s
7 | 1: %2$s 2: %1$s 3: %1$s 4: %s 5: %3$s 6: %s
8 | 1: %%2$s 2: %1$s 3: %1$s 4: %%s 5: %3$s 6: %s
9 |
10 | - %1$s test item
11 | - %1$s test items
12 |
13 |
14 | - %1$s found %2$s %3$s item
15 | - %1$s found %2$s %3$s items
16 |
17 |
18 | - 1: %s 2: %1$s 3: %s item
19 | - 1: %s 2: %1$s 3: %s items
20 |
21 |
22 | - 1: %2$s 2: %1$s 3: %1$s 4: %s 5: %3$s 6: %s item
23 | - 1: %2$s 2: %1$s 3: %1$s 4: %s 5: %3$s 6: %s items
24 |
25 |
26 | - 1: %%2$s 2: %1$s 3: %1$s 4: %%s 5: %3$s 6: %s item
27 | - 1: %%2$s 2: %1$s 3: %1$s 4: %%s 5: %3$s 6: %s items
28 |
29 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform/src/desktopTest/composeResources/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Instrumented test string
4 | Hello from %1$s test!
5 | %1$s has %2$s %3$s
6 | 1: %s 2: %1$s 3: %s
7 | 1: %2$s 2: %1$s 3: %1$s 4: %s 5: %3$s 6: %s
8 | 1: %%2$s 2: %1$s 3: %1$s 4: %%s 5: %3$s 6: %s
9 |
10 | - %1$s test item
11 | - %1$s test items
12 |
13 |
14 | - %1$s found %2$s %3$s item
15 | - %1$s found %2$s %3$s items
16 |
17 |
18 | - 1: %s 2: %1$s 3: %s item
19 | - 1: %s 2: %1$s 3: %s items
20 |
21 |
22 | - 1: %2$s 2: %1$s 3: %1$s 4: %s 5: %3$s 6: %s item
23 | - 1: %2$s 2: %1$s 3: %1$s 4: %s 5: %3$s 6: %s items
24 |
25 |
26 | - 1: %%2$s 2: %1$s 3: %1$s 4: %%s 5: %3$s 6: %s item
27 | - 1: %%2$s 2: %1$s 3: %1$s 4: %%s 5: %3$s 6: %s items
28 |
29 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.radusalagean.uitextcompose.Config
2 |
3 | plugins {
4 | alias(libs.plugins.android.application)
5 | alias(libs.plugins.kotlin.android)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | android {
10 | namespace = "${Config.rootPackage}.android.sample"
11 | compileSdk = Config.compileSdk
12 |
13 | defaultConfig {
14 | applicationId = "${Config.rootPackage}.android.sample"
15 | minSdk = Config.minSdk
16 | targetSdk = Config.targetSdk
17 | versionCode = Config.versionCode
18 | versionName = Config.versionName
19 | }
20 |
21 | buildTypes {
22 | release {
23 | isMinifyEnabled = false
24 | proguardFiles(
25 | getDefaultProguardFile("proguard-android-optimize.txt"),
26 | "proguard-rules.pro"
27 | )
28 | }
29 | }
30 | compileOptions {
31 | sourceCompatibility = JavaVersion.VERSION_11
32 | targetCompatibility = JavaVersion.VERSION_11
33 | }
34 | kotlinOptions {
35 | jvmTarget = "11"
36 | }
37 | buildFeatures {
38 | compose = true
39 | }
40 | androidResources {
41 | generateLocaleConfig = true
42 | localeFilters += listOf("en", "ro")
43 | }
44 | }
45 |
46 | dependencies {
47 | implementation(project(":uitextcompose-android"))
48 | implementation(libs.core.ktx)
49 | implementation(libs.lifecycle.runtime.ktx)
50 | implementation(libs.androidx.activity.compose)
51 | implementation(libs.androidx.compose.ui)
52 | implementation(libs.androidx.compose.ui.graphics)
53 | implementation(libs.androidx.compose.ui.tooling.preview)
54 | implementation(libs.androidx.compose.material3)
55 | implementation (libs.androidx.appcompat)
56 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/uitextcompose-android/api/uitextcompose-android.api:
--------------------------------------------------------------------------------
1 | public abstract class com/radusalagean/uitextcompose/android/UIText : com/radusalagean/uitextcompose/core/UITextBase {
2 | public static final field $stable I
3 | protected abstract fun build (Landroid/content/Context;)Ljava/lang/CharSequence;
4 | public final fun buildAnnotatedString (Landroid/content/Context;)Landroidx/compose/ui/text/AnnotatedString;
5 | public fun buildAnnotatedStringComposable (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/text/AnnotatedString;
6 | public final fun buildString (Landroid/content/Context;)Ljava/lang/String;
7 | public fun buildStringComposable (Landroidx/compose/runtime/Composer;I)Ljava/lang/String;
8 | protected final fun hasAnnotations (Ljava/util/List;Ljava/util/List;Ljava/util/List;)Z
9 | protected final fun resolveArg (Landroid/content/Context;Ljava/lang/Object;)Ljava/lang/Object;
10 | }
11 |
12 | public final class com/radusalagean/uitextcompose/android/UITextBuilder : com/radusalagean/uitextcompose/core/UITextBuilderBase {
13 | public static final field $stable I
14 | public fun ()V
15 | public fun build ()Lcom/radusalagean/uitextcompose/android/UIText;
16 | public synthetic fun build ()Ljava/lang/Object;
17 | public final fun pluralRes (IILkotlin/jvm/functions/Function1;)V
18 | public static synthetic fun pluralRes$default (Lcom/radusalagean/uitextcompose/android/UITextBuilder;IILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
19 | public fun raw (Ljava/lang/CharSequence;)V
20 | public final fun res (ILkotlin/jvm/functions/Function1;)V
21 | public static synthetic fun res$default (Lcom/radusalagean/uitextcompose/android/UITextBuilder;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
22 | }
23 |
24 | public final class com/radusalagean/uitextcompose/android/UITextBuilderKt {
25 | public static final fun UIText (Lkotlin/jvm/functions/Function1;)Lcom/radusalagean/uitextcompose/android/UIText;
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | CADisableMinimumFrameDurationOnPhone
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 |
30 | UILaunchScreen
31 |
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 | UISupportedInterfaceOrientations~ipad
43 |
44 | UIInterfaceOrientationPortrait
45 | UIInterfaceOrientationPortraitUpsideDown
46 | UIInterfaceOrientationLandscapeLeft
47 | UIInterfaceOrientationLandscapeRight
48 |
49 | CFBundleLocalizations
50 |
51 | en
52 | ro
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/uitextcompose-android/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.radusalagean.uitextcompose.Config
2 | import com.vanniktech.maven.publish.SonatypeHost
3 |
4 | plugins {
5 | alias(libs.plugins.kotlin.multiplatform)
6 | alias(libs.plugins.kotlin.binaryCompatibilityValidator)
7 | alias(libs.plugins.android.library)
8 | alias(libs.plugins.compose.compiler)
9 | alias(libs.plugins.vanniktech.mavenPublish)
10 | }
11 |
12 | group = Config.artifactGroup
13 | version = Config.versionName
14 |
15 | kotlin {
16 | androidTarget {
17 | publishLibraryVariants("release")
18 | }
19 |
20 | sourceSets {
21 | val androidMain by getting {
22 | dependencies {
23 | api(project(":uitextcompose-core"))
24 | api(libs.androidx.annotation)
25 | api(libs.androidx.compose.runtime)
26 | api(libs.androidx.compose.ui)
27 | }
28 | }
29 | val androidInstrumentedTest by getting {
30 | dependencies {
31 | implementation(libs.androidx.compose.ui.test.manifest)
32 | implementation(libs.kotlin.test)
33 | implementation(libs.ext.junit)
34 | implementation(libs.espresso.core)
35 | implementation(libs.androidx.compose.ui.test.junit4)
36 | implementation(libs.kotlinx.coroutines.test)
37 | }
38 | }
39 | }
40 | explicitApi()
41 | }
42 |
43 | android {
44 | namespace = "${Config.rootPackage}.android"
45 | compileSdk = Config.compileSdk
46 | defaultConfig {
47 | minSdk = Config.minSdk
48 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
49 | }
50 | compileOptions {
51 | sourceCompatibility = JavaVersion.VERSION_11
52 | targetCompatibility = JavaVersion.VERSION_11
53 | }
54 | }
55 |
56 | mavenPublishing {
57 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
58 |
59 | signAllPublications()
60 |
61 | coordinates(group.toString(), "ui-text-compose-android", version.toString())
62 | }
63 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.radusalagean.uitextcompose.Config
2 | import com.vanniktech.maven.publish.SonatypeHost
3 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
4 |
5 | plugins {
6 | alias(libs.plugins.kotlin.multiplatform)
7 | alias(libs.plugins.kotlin.binaryCompatibilityValidator)
8 | alias(libs.plugins.android.library)
9 | alias(libs.plugins.compose.compiler)
10 | alias(libs.plugins.compose.multiplatform)
11 | alias(libs.plugins.vanniktech.mavenPublish)
12 | }
13 |
14 | group = Config.artifactGroup
15 | version = Config.versionName
16 |
17 | kotlin {
18 | jvm("desktop")
19 | androidTarget {
20 | publishLibraryVariants("release")
21 | }
22 | iosX64()
23 | iosArm64()
24 | iosSimulatorArm64()
25 | @OptIn(ExperimentalWasmDsl::class)
26 | wasmJs {
27 | browser()
28 | }
29 |
30 | sourceSets {
31 | val commonMain by getting {
32 | dependencies {
33 | api(project(":uitextcompose-core"))
34 | api(compose.runtime)
35 | api(compose.ui)
36 | api(compose.components.resources)
37 | }
38 | }
39 | val desktopTest by getting {
40 | dependencies {
41 | implementation(libs.kotlin.test)
42 | implementation(compose.desktop.currentOs)
43 | implementation(compose.desktop.uiTestJUnit4)
44 | }
45 | }
46 | }
47 | explicitApi()
48 | }
49 |
50 | android {
51 | namespace = "${Config.rootPackage}.multiplatform"
52 | compileSdk = Config.compileSdk
53 | defaultConfig {
54 | minSdk = Config.minSdk
55 | }
56 | compileOptions {
57 | sourceCompatibility = JavaVersion.VERSION_11
58 | targetCompatibility = JavaVersion.VERSION_11
59 | }
60 | }
61 |
62 | mavenPublishing {
63 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
64 |
65 | signAllPublications()
66 |
67 | coordinates(group.toString(), "ui-text-compose-multiplatform", version.toString())
68 | }
69 |
--------------------------------------------------------------------------------
/uitextcompose-core/src/commonMain/kotlin/com/radusalagean/uitextcompose/core/UITextBase.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.core
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.text.AnnotatedString
5 |
6 | /**
7 | * Base interface for text representation in Compose UI.
8 | *
9 | * This interface defines the core functionality for text that can be rendered in Compose UI.
10 | * Implementations can retrieve text from various sources (raw strings, resource files, etc.)
11 | * and provide both plain text and annotated text output in a composable context.
12 | *
13 | * Example usage:
14 | * ```
15 | * val myText: UITextBase = // implementation of UITextBase
16 | *
17 | * @Composable
18 | * fun DisplayText(text: UITextBase) {
19 | * // Get a plain string
20 | * Text(text = text.buildStringComposable())
21 | *
22 | * // Or get an annotated string with styling
23 | * Text(text = text.buildAnnotatedStringComposable())
24 | * }
25 | * ```
26 | *
27 | * @see buildStringComposable
28 | * @see buildAnnotatedStringComposable
29 | */
30 | public interface UITextBase {
31 | /**
32 | * Builds and returns a plain text string in a composable context.
33 | *
34 | * This function resolves the text content from its source (e.g., string resources, raw text)
35 | * and returns it as a plain string. Any styling information will be lost.
36 | *
37 | * @return A plain string representation of the text content.
38 | */
39 | @Composable
40 | public fun buildStringComposable(): String
41 |
42 | /**
43 | * Builds and returns an annotated string with styling in a composable context.
44 | *
45 | * This function resolves the text content from its source (e.g., string resources, raw text)
46 | * and returns it as an [AnnotatedString] that preserves styling information like spans,
47 | * paragraph styles, and link annotations.
48 | *
49 | * @return An [AnnotatedString] representation of the text content with styling information.
50 | */
51 | @Composable
52 | public fun buildAnnotatedStringComposable(): AnnotatedString
53 | }
54 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform/api/android/uitextcompose-multiplatform.api:
--------------------------------------------------------------------------------
1 | public abstract class com/radusalagean/uitextcompose/multiplatform/UIText : com/radusalagean/uitextcompose/core/UITextBase {
2 | public static final field $stable I
3 | protected abstract fun build (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
4 | public final fun buildAnnotatedString (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
5 | public fun buildAnnotatedStringComposable (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/text/AnnotatedString;
6 | public final fun buildString (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
7 | public fun buildStringComposable (Landroidx/compose/runtime/Composer;I)Ljava/lang/String;
8 | protected final fun hasAnnotations (Ljava/util/List;Ljava/util/List;Ljava/util/List;)Z
9 | protected final fun resolveArg (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
10 | }
11 |
12 | public final class com/radusalagean/uitextcompose/multiplatform/UITextBuilder : com/radusalagean/uitextcompose/core/UITextBuilderBase {
13 | public static final field $stable I
14 | public fun ()V
15 | public fun build ()Lcom/radusalagean/uitextcompose/multiplatform/UIText;
16 | public synthetic fun build ()Ljava/lang/Object;
17 | public final fun pluralRes (Lorg/jetbrains/compose/resources/PluralStringResource;ILkotlin/jvm/functions/Function1;)V
18 | public static synthetic fun pluralRes$default (Lcom/radusalagean/uitextcompose/multiplatform/UITextBuilder;Lorg/jetbrains/compose/resources/PluralStringResource;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
19 | public fun raw (Ljava/lang/CharSequence;)V
20 | public final fun res (Lorg/jetbrains/compose/resources/StringResource;Lkotlin/jvm/functions/Function1;)V
21 | public static synthetic fun res$default (Lcom/radusalagean/uitextcompose/multiplatform/UITextBuilder;Lorg/jetbrains/compose/resources/StringResource;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
22 | }
23 |
24 | public final class com/radusalagean/uitextcompose/multiplatform/UITextBuilderKt {
25 | public static final fun UIText (Lkotlin/jvm/functions/Function1;)Lcom/radusalagean/uitextcompose/multiplatform/UIText;
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform/api/desktop/uitextcompose-multiplatform.api:
--------------------------------------------------------------------------------
1 | public abstract class com/radusalagean/uitextcompose/multiplatform/UIText : com/radusalagean/uitextcompose/core/UITextBase {
2 | public static final field $stable I
3 | protected abstract fun build (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
4 | public final fun buildAnnotatedString (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
5 | public fun buildAnnotatedStringComposable (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/text/AnnotatedString;
6 | public final fun buildString (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
7 | public fun buildStringComposable (Landroidx/compose/runtime/Composer;I)Ljava/lang/String;
8 | protected final fun hasAnnotations (Ljava/util/List;Ljava/util/List;Ljava/util/List;)Z
9 | protected final fun resolveArg (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
10 | }
11 |
12 | public final class com/radusalagean/uitextcompose/multiplatform/UITextBuilder : com/radusalagean/uitextcompose/core/UITextBuilderBase {
13 | public static final field $stable I
14 | public fun ()V
15 | public fun build ()Lcom/radusalagean/uitextcompose/multiplatform/UIText;
16 | public synthetic fun build ()Ljava/lang/Object;
17 | public final fun pluralRes (Lorg/jetbrains/compose/resources/PluralStringResource;ILkotlin/jvm/functions/Function1;)V
18 | public static synthetic fun pluralRes$default (Lcom/radusalagean/uitextcompose/multiplatform/UITextBuilder;Lorg/jetbrains/compose/resources/PluralStringResource;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
19 | public fun raw (Ljava/lang/CharSequence;)V
20 | public final fun res (Lorg/jetbrains/compose/resources/StringResource;Lkotlin/jvm/functions/Function1;)V
21 | public static synthetic fun res$default (Lcom/radusalagean/uitextcompose/multiplatform/UITextBuilder;Lorg/jetbrains/compose/resources/StringResource;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
22 | }
23 |
24 | public final class com/radusalagean/uitextcompose/multiplatform/UITextBuilderKt {
25 | public static final fun UIText (Lkotlin/jvm/functions/Function1;)Lcom/radusalagean/uitextcompose/multiplatform/UIText;
26 | }
27 |
28 |
--------------------------------------------------------------------------------
/uitextcompose-core/src/commonMain/kotlin/com/radusalagean/uitextcompose/core/UITextAnnotation.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.core
2 |
3 | import androidx.compose.ui.text.LinkAnnotation
4 | import androidx.compose.ui.text.ParagraphStyle
5 | import androidx.compose.ui.text.SpanStyle
6 | import kotlin.jvm.JvmInline
7 |
8 | /**
9 | * Represents text annotations for styling and linking.
10 | *
11 | * This sealed interface provides a type-safe way to represent different kinds of
12 | * text annotations used for styling and formatting text in Compose UI.
13 | *
14 | * The library uses these annotations internally to apply styling to text. Users
15 | * typically add annotations through the DSL rather than creating instances directly.
16 | *
17 | * @see Span
18 | * @see Paragraph
19 | * @see Link
20 | */
21 | @InternalApi
22 | public sealed interface UITextAnnotation {
23 |
24 | /**
25 | * Represents character-level styling for text.
26 | *
27 | * Span styles apply to individual characters and can include properties like
28 | * color, font weight, font style, letter spacing, etc.
29 | *
30 | * @property spanStyle The Compose UI [SpanStyle] to apply to the text.
31 | */
32 | @JvmInline
33 | public value class Span(
34 | internal val spanStyle: SpanStyle
35 | ) : UITextAnnotation
36 |
37 | /**
38 | * Represents paragraph-level styling for text.
39 | *
40 | * Paragraph styles apply to entire paragraphs and can include properties like
41 | * alignment, indentation, line height, etc.
42 | *
43 | * @property paragraphStyle The Compose UI [ParagraphStyle] to apply to the text.
44 | */
45 | @JvmInline
46 | public value class Paragraph(
47 | internal val paragraphStyle: ParagraphStyle
48 | ) : UITextAnnotation
49 |
50 | /**
51 | * Represents a link annotation for text.
52 | *
53 | * Link annotations can be used to make portions of text clickable and associate
54 | * metadata like URLs with them.
55 | *
56 | * @property linkAnnotation The Compose UI [LinkAnnotation] to apply to the text.
57 | */
58 | @JvmInline
59 | public value class Link(
60 | internal val linkAnnotation: LinkAnnotation
61 | ) : UITextAnnotation
62 | }
--------------------------------------------------------------------------------
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
7 |
8 | name: Java CI with Gradle
9 |
10 | on:
11 | push:
12 | branches: [ "main" ]
13 | pull_request:
14 | branches: [ "main" ]
15 | workflow_call:
16 |
17 | permissions:
18 | contents: read
19 |
20 | jobs:
21 | test:
22 | strategy:
23 | matrix:
24 | module: [uitextcompose-core, uitextcompose-android, uitextcompose-multiplatform]
25 | os: [ubuntu-latest]
26 |
27 | runs-on: ${{ matrix.os }}
28 |
29 | steps:
30 | - uses: actions/checkout@v3
31 |
32 | - name: Install dependencies
33 | run: |
34 | sudo apt-get update
35 | sudo apt-get install -y \
36 | libgl1 \
37 | xvfb \
38 | libxtst6 \
39 | libxi6
40 |
41 | - name: Set up JDK 17
42 | uses: actions/setup-java@v3
43 | with:
44 | java-version: '17'
45 | distribution: 'temurin'
46 | cache: gradle
47 |
48 | - name: Enable KVM
49 | if: matrix.module == 'uitextcompose-android'
50 | run: |
51 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
52 | sudo udevadm control --reload-rules
53 | sudo udevadm trigger --name-match=kvm
54 |
55 | - name: Setup Android SDK
56 | if: matrix.module == 'uitextcompose-android'
57 | uses: android-actions/setup-android@v3
58 | with:
59 | log-accepted-android-sdk-licenses: false
60 |
61 | - name: Validate Gradle Wrapper
62 | uses: gradle/actions/wrapper-validation@v3
63 |
64 | - name: Setup Gradle
65 | uses: gradle/actions/setup-gradle@v3
66 |
67 | - name: Execute Core Check
68 | if: matrix.module == 'uitextcompose-core'
69 | run: ./gradlew uitextcompose-core:check
70 |
71 | - name: Execute Multiplatform Tests
72 | if: matrix.module == 'uitextcompose-multiplatform'
73 | run: xvfb-run ./gradlew uitextcompose-multiplatform:check uitextcompose-multiplatform:desktopTest
74 |
75 | - name: Execute Android Tests
76 | if: matrix.module == 'uitextcompose-android'
77 | uses: reactivecircus/android-emulator-runner@v2
78 | with:
79 | api-level: 29
80 | target: google_apis
81 | arch: x86_64
82 | profile: Nexus 6
83 | script: xvfb-run ./gradlew uitextcompose-android:check uitextcompose-android:connectedAndroidTest
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo.
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
48 | echo.
49 | echo Please set the JAVA_HOME variable in your environment to match the
50 | echo location of your Java installation.
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo.
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
62 | echo.
63 | echo Please set the JAVA_HOME variable in your environment to match the
64 | echo location of your Java installation.
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/screen/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.screen
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.verticalScroll
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.InputChip
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.material3.Text
16 | import androidx.compose.material3.TopAppBar
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.LaunchedEffect
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.platform.LocalConfiguration
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.compose.ui.unit.dp
24 | import com.radusalagean.uitextcompose.android.sample.R
25 | import com.radusalagean.uitextcompose.android.sample.ui.component.ExampleEntry
26 | import com.radusalagean.uitextcompose.android.sample.ui.component.Section
27 |
28 | @Composable
29 | @OptIn(ExperimentalMaterial3Api::class)
30 | fun MainScreen(
31 | viewModel: MainViewModel,
32 | modifier: Modifier = Modifier
33 | ) {
34 | val localConfiguration = LocalConfiguration.current
35 | LaunchedEffect(localConfiguration) {
36 | viewModel.syncSelectedLanguage()
37 | }
38 | MaterialTheme {
39 | Scaffold(modifier = modifier.fillMaxSize()) { innerPadding ->
40 | Column(Modifier.fillMaxSize().padding(innerPadding)) {
41 | TopAppBar(
42 | title = {
43 | Text(stringResource(R.string.app_name))
44 | }
45 | )
46 | Column(
47 | modifier = Modifier
48 | .padding(horizontal = 16.dp)
49 | .fillMaxWidth()
50 | .verticalScroll(rememberScrollState())
51 | .padding(bottom = 42.dp)
52 | ) {
53 | Section(title = viewModel.languageSectionTitle) {
54 | Row(
55 | horizontalArrangement = Arrangement.spacedBy(8.dp),
56 | verticalAlignment = Alignment.CenterVertically,
57 | modifier = Modifier.padding(vertical = 8.dp)
58 | ) {
59 | viewModel.languageOptions.forEachIndexed { index, entry ->
60 | InputChip(
61 | selected = index == viewModel.selectedLanguageIndex,
62 | onClick = { viewModel.onLanguageSelected(entry.languageCode) },
63 | label = {
64 | Text(entry.uiText.buildAnnotatedStringComposable())
65 | }
66 | )
67 | }
68 | }
69 | }
70 |
71 | Section(title = viewModel.examplesSectionTitle) {
72 | Column(
73 | verticalArrangement = Arrangement.spacedBy(16.dp)
74 | ) {
75 | viewModel.exampleEntries.forEach {
76 | ExampleEntry(
77 | model = it
78 | )
79 | }
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.10.0"
3 | kotlin = "2.1.20"
4 | kotlin-binaryCompatibilityValidator = "0.17.0"
5 | kotlinx-coroutines = "1.10.2"
6 | compose-multiplatform = "1.8.0"
7 | coreKtx = "1.16.0"
8 | junitVersion = "1.2.1"
9 | espressoCore = "3.6.1"
10 | lifecycleRuntimeKtx = "2.9.0"
11 | activityCompose = "1.10.1"
12 | compose = "1.8.1"
13 | compose-material3 = "1.3.2"
14 | androidx-lifecycle = "2.8.4"
15 | androidx-appcompat = "1.7.0"
16 | androidx-annotation = "1.9.1"
17 | napier = "2.7.1"
18 | koin = "4.0.4"
19 |
20 | [libraries]
21 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
22 | core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
23 | ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
24 | espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
25 | lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
26 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
27 | androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "compose" }
28 | androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose" }
29 | androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics", version.ref = "compose" }
30 | androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling", version.ref = "compose" }
31 | androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose" }
32 | androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest", version.ref = "compose" }
33 | androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "compose" }
34 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" }
35 | kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
36 | kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
37 | androidx-lifecycle-viewmodel-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
38 | androidx-lifecycle-runtime-compose = { group = "org.jetbrains.androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
39 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
40 | androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidx-annotation" }
41 | napier = { group = "io.github.aakira", name = "napier", version.ref = "napier" }
42 | koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin" }
43 | koin-core = { module = "io.insert-koin:koin-core" }
44 | koin-compose = { module = "io.insert-koin:koin-compose" }
45 | koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel" }
46 |
47 | [plugins]
48 | android-application = { id = "com.android.application", version.ref = "agp" }
49 | android-library = { id = "com.android.library", version.ref = "agp" }
50 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
51 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
52 | kotlin-binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "kotlin-binaryCompatibilityValidator" }
53 | compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
54 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
55 | vanniktech-mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.31.0" }
56 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/android-sample.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/multiplatform_sample_android.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.radusalagean.uitextcompose.Config
2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
3 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
4 | import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
5 |
6 | plugins {
7 | alias(libs.plugins.android.application)
8 | alias(libs.plugins.kotlin.multiplatform)
9 | alias(libs.plugins.compose.multiplatform)
10 | alias(libs.plugins.compose.compiler)
11 | }
12 |
13 | kotlin {
14 | androidTarget {
15 | compilerOptions {
16 | jvmTarget.set(JvmTarget.JVM_11)
17 | }
18 | }
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach { iosTarget ->
25 | iosTarget.binaries.framework {
26 | baseName = "shared"
27 | isStatic = true
28 | }
29 | }
30 |
31 | jvm("desktop")
32 |
33 | @OptIn(ExperimentalWasmDsl::class)
34 | wasmJs {
35 | browser {
36 | val rootDirPath = project.rootDir.path
37 | val projectDirPath = project.projectDir.path
38 | commonWebpackConfig {
39 | outputFileName = "uitextcompose-multiplatform-sample.js"
40 | devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
41 | static = (static ?: mutableListOf()).apply {
42 | // Serve sources to debug inside browser
43 | add(rootDirPath)
44 | add(projectDirPath)
45 | }
46 | }
47 | }
48 | }
49 | binaries.executable()
50 | }
51 |
52 | sourceSets {
53 | val desktopMain by getting
54 |
55 | androidMain.dependencies {
56 | implementation(compose.preview)
57 | implementation(libs.androidx.activity.compose)
58 | implementation(libs.androidx.appcompat)
59 | }
60 | commonMain.dependencies {
61 | implementation(project(":uitextcompose-multiplatform"))
62 | implementation(compose.runtime)
63 | implementation(compose.foundation)
64 | implementation(compose.material3)
65 | implementation(compose.ui)
66 | implementation(compose.components.resources)
67 | implementation(compose.components.uiToolingPreview)
68 | implementation(libs.androidx.lifecycle.viewmodel.compose)
69 | implementation(libs.androidx.lifecycle.runtime.compose)
70 | implementation(libs.napier)
71 | implementation(project.dependencies.platform(libs.koin.bom))
72 | implementation(libs.koin.core)
73 | implementation(libs.koin.compose)
74 | implementation(libs.koin.compose.viewmodel)
75 | }
76 | desktopMain.dependencies {
77 | implementation(compose.desktop.currentOs)
78 | implementation(libs.kotlinx.coroutines.swing)
79 | }
80 | }
81 | }
82 |
83 | android {
84 | namespace = "${Config.rootPackage}.multiplatform.sample"
85 | compileSdk = Config.compileSdk
86 |
87 | defaultConfig {
88 | applicationId = "${Config.rootPackage}.multiplatform.sample"
89 | minSdk = Config.minSdk
90 | targetSdk = Config.targetSdk
91 | versionCode = Config.versionCode
92 | versionName = Config.versionName
93 | }
94 | packaging {
95 | resources {
96 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
97 | }
98 | }
99 | buildTypes {
100 | getByName("release") {
101 | isMinifyEnabled = false
102 | }
103 | }
104 | compileOptions {
105 | sourceCompatibility = JavaVersion.VERSION_11
106 | targetCompatibility = JavaVersion.VERSION_11
107 | }
108 | }
109 |
110 | dependencies {
111 | debugImplementation(compose.uiTooling)
112 | }
113 |
114 | compose.desktop {
115 | application {
116 | mainClass = "com.radusalagean.uitextcompose.multiplatform.sample.Main_desktopKt"
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/screen/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.screen
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.verticalScroll
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.InputChip
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.material3.Text
16 | import androidx.compose.material3.TopAppBar
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.LaunchedEffect
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.unit.dp
22 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.component.ExampleEntry
23 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.component.Section
24 | import org.jetbrains.compose.resources.rememberResourceEnvironment
25 | import org.jetbrains.compose.resources.stringResource
26 | import org.koin.compose.viewmodel.koinViewModel
27 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.Res
28 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.app_name
29 |
30 | @Composable
31 | @OptIn(ExperimentalMaterial3Api::class)
32 | fun MainScreen(
33 | viewModel: MainViewModel = koinViewModel(),
34 | modifier: Modifier = Modifier,
35 | languagePickerEnabled: Boolean = true
36 | ) {
37 | val resourceEnvironment = rememberResourceEnvironment()
38 | LaunchedEffect(resourceEnvironment) {
39 | viewModel.syncSelectedLanguage()
40 | }
41 | MaterialTheme {
42 | Scaffold(modifier = modifier.fillMaxSize()) { innerPadding ->
43 | Column(Modifier.fillMaxSize().padding(innerPadding)) {
44 | TopAppBar(
45 | title = {
46 | Text(stringResource(Res.string.app_name))
47 | }
48 | )
49 | Column(
50 | modifier = Modifier
51 | .padding(horizontal = 16.dp)
52 | .fillMaxWidth()
53 | .verticalScroll(rememberScrollState())
54 | .padding(bottom = 42.dp)
55 | ) {
56 | Section(title = viewModel.languageSectionTitle) {
57 | Row(
58 | horizontalArrangement = Arrangement.spacedBy(8.dp),
59 | verticalAlignment = Alignment.CenterVertically,
60 | modifier = Modifier.padding(vertical = 8.dp)
61 | ) {
62 | viewModel.languageOptions.forEachIndexed { index, entry ->
63 | InputChip(
64 | selected = index == viewModel.selectedLanguageIndex,
65 | onClick = { viewModel.onLanguageSelected(entry.languageCode) },
66 | label = {
67 | Text(entry.uiText.buildStringComposable())
68 | },
69 | enabled = languagePickerEnabled
70 | )
71 | }
72 | }
73 | }
74 |
75 | Section(title = viewModel.examplesSectionTitle) {
76 | Column(
77 | verticalArrangement = Arrangement.spacedBy(16.dp)
78 | ) {
79 | viewModel.exampleEntries.forEach {
80 | ExampleEntry(
81 | model = it
82 | )
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 | }
90 | }
--------------------------------------------------------------------------------
/uitextcompose-android/src/androidMain/kotlin/com/radusalagean/uitextcompose/android/UITextBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android
2 |
3 | import androidx.annotation.PluralsRes
4 | import androidx.annotation.StringRes
5 | import com.radusalagean.uitextcompose.core.InternalApi
6 | import com.radusalagean.uitextcompose.core.ResBuilder
7 | import com.radusalagean.uitextcompose.core.UITextBuilderBase
8 | import com.radusalagean.uitextcompose.core.UITextDslMarker
9 |
10 | /**
11 | * Creates a new [UIText] instance using a DSL builder.
12 | *
13 | * This function provides a concise way to create text from various sources including
14 | * raw text and Android string resources. It supports styling and formatting through
15 | * a type-safe DSL.
16 | *
17 | * Example:
18 | * ```
19 | * // Create from raw text
20 | * val text1 = UIText {
21 | * raw("Hello, World!")
22 | * }
23 | *
24 | * // Create from string resource
25 | * val text2 = UIText {
26 | * res(R.string.greeting) {
27 | * arg("User")
28 | * }
29 | * }
30 | *
31 | * // Create from plural resource
32 | * val text3 = UIText {
33 | * pluralRes(R.plurals.items_count, 5)
34 | * }
35 | *
36 | * // Combine multiple sources
37 | * val text4 = UIText {
38 | * raw("Hello, ")
39 | * res(R.string.user_name)
40 | * raw("!")
41 | * }
42 | * ```
43 | *
44 | * @param block The builder block for configuring the text.
45 | * @return A new [UIText] instance.
46 | */
47 | public fun UIText(block: UITextBuilder.() -> Unit): UIText = UITextBuilder().apply(block).build()
48 |
49 | /**
50 | * Builder for creating [UIText] instances specifically for Android.
51 | *
52 | * This builder allows creating text from multiple sources including raw text and
53 | * Android string resources. It supports styling and formatting through annotations.
54 | *
55 | * The builder collects text components and combines them into a single [UIText] instance.
56 | *
57 | * @see UIText
58 | * @see raw
59 | * @see res
60 | * @see pluralRes
61 | */
62 | @UITextDslMarker
63 | @OptIn(InternalApi::class)
64 | public class UITextBuilder : UITextBuilderBase {
65 | private val components = mutableListOf()
66 |
67 | /**
68 | * Adds raw text content to the builder.
69 | *
70 | * Use this method to add plain text that doesn't come from a resource.
71 | *
72 | * Example:
73 | * ```
74 | * UIText {
75 | * raw("Hello, World!")
76 | * }
77 | * ```
78 | *
79 | * @param text The text content to add.
80 | */
81 | override fun raw(text: CharSequence) {
82 | components += UIText.Raw(text)
83 | }
84 |
85 | /**
86 | * Adds text from an Android string resource.
87 | *
88 | * This method resolves text from an Android string resource ID and allows adding
89 | * arguments for placeholders in the resource string.
90 | *
91 | * Example:
92 | * ```
93 | * UIText {
94 | * res(R.string.greeting) {
95 | * arg("User") {
96 | * +SpanStyle(color = Color.Blue)
97 | * }
98 | * }
99 | * }
100 | * ```
101 | *
102 | * @param resId The Android string resource ID to use.
103 | * @param resBuilder Optional builder for configuring arguments and styling.
104 | */
105 | public fun res(
106 | @StringRes resId: Int,
107 | resBuilder: ResBuilder.() -> Unit = { }
108 | ) {
109 | val config = ResBuilder().apply(resBuilder).buildResConfig()
110 | components += UIText.Res(
111 | resId = resId,
112 | args = config.args,
113 | baseAnnotations = config.annotations
114 | )
115 | }
116 |
117 | /**
118 | * Adds text from an Android plural resource.
119 | *
120 | * This method resolves text from an Android plural resource ID based on the given quantity
121 | * and allows adding arguments for placeholders in the resource string.
122 | *
123 | * By default, the quantity is automatically added as the first argument.
124 | *
125 | * Example:
126 | * ```
127 | * UIText {
128 | * pluralRes(R.plurals.items_count, 5) {
129 | * // Additional arguments if needed
130 | * arg("category") {
131 | * +SpanStyle(fontWeight = FontWeight.Bold)
132 | * }
133 | * }
134 | * }
135 | * ```
136 | *
137 | * @param resId The Android plural resource ID to use.
138 | * @param quantity The quantity value for selecting the appropriate plural form.
139 | * @param resBuilder Optional builder for configuring arguments and styling.
140 | */
141 | public fun pluralRes(
142 | @PluralsRes resId: Int,
143 | quantity: Int,
144 | resBuilder: ResBuilder.() -> Unit = {
145 | arg(quantity.toString())
146 | }
147 | ) {
148 | val config = ResBuilder().apply(resBuilder).buildResConfig()
149 | components += UIText.PluralRes(
150 | resId = resId,
151 | quantity = quantity,
152 | args = config.args,
153 | baseAnnotations = config.annotations
154 | )
155 | }
156 |
157 | /**
158 | * Builds and returns the final [UIText] instance.
159 | *
160 | * This method creates an appropriate [UIText] instance based on the components
161 | * added to the builder:
162 | * - If no components were added, returns an empty text.
163 | * - If only one component was added, returns that component directly.
164 | * - If multiple components were added, returns a compound text that combines them.
165 | *
166 | * @return The built [UIText] instance.
167 | */
168 | override fun build(): UIText = when (components.size) {
169 | 0 -> UIText.Raw("")
170 | 1 -> components[0]
171 | else -> UIText.Compound(components)
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/uitextcompose-core/api/android/uitextcompose-core.api:
--------------------------------------------------------------------------------
1 | public class com/radusalagean/uitextcompose/core/AnnotationsBuilder {
2 | public static final field $stable I
3 | public fun ()V
4 | public final fun buildAnnotations ()Ljava/util/List;
5 | public final fun unaryPlus (Landroidx/compose/ui/text/LinkAnnotation;)V
6 | public final fun unaryPlus (Landroidx/compose/ui/text/ParagraphStyle;)V
7 | public final fun unaryPlus (Landroidx/compose/ui/text/SpanStyle;)V
8 | }
9 |
10 | public abstract interface annotation class com/radusalagean/uitextcompose/core/InternalApi : java/lang/annotation/Annotation {
11 | }
12 |
13 | public final class com/radusalagean/uitextcompose/core/ResBuilder : com/radusalagean/uitextcompose/core/AnnotationsBuilder {
14 | public static final field $stable I
15 | public fun ()V
16 | public final fun arg (Lcom/radusalagean/uitextcompose/core/UITextBase;Lkotlin/jvm/functions/Function1;)V
17 | public final fun arg (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)V
18 | public static synthetic fun arg$default (Lcom/radusalagean/uitextcompose/core/ResBuilder;Lcom/radusalagean/uitextcompose/core/UITextBase;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
19 | public static synthetic fun arg$default (Lcom/radusalagean/uitextcompose/core/ResBuilder;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
20 | public final fun buildResConfig ()Lcom/radusalagean/uitextcompose/core/ResConfig;
21 | }
22 |
23 | public final class com/radusalagean/uitextcompose/core/ResConfig {
24 | public static final field $stable I
25 | public fun (Ljava/util/List;Ljava/util/List;)V
26 | public final fun getAnnotations ()Ljava/util/List;
27 | public final fun getArgs ()Ljava/util/List;
28 | }
29 |
30 | public abstract interface class com/radusalagean/uitextcompose/core/UITextAnnotation {
31 | }
32 |
33 | public final class com/radusalagean/uitextcompose/core/UITextAnnotation$Link : com/radusalagean/uitextcompose/core/UITextAnnotation {
34 | public static final synthetic fun box-impl (Landroidx/compose/ui/text/LinkAnnotation;)Lcom/radusalagean/uitextcompose/core/UITextAnnotation$Link;
35 | public static fun constructor-impl (Landroidx/compose/ui/text/LinkAnnotation;)Landroidx/compose/ui/text/LinkAnnotation;
36 | public fun equals (Ljava/lang/Object;)Z
37 | public static fun equals-impl (Landroidx/compose/ui/text/LinkAnnotation;Ljava/lang/Object;)Z
38 | public static final fun equals-impl0 (Landroidx/compose/ui/text/LinkAnnotation;Landroidx/compose/ui/text/LinkAnnotation;)Z
39 | public fun hashCode ()I
40 | public static fun hashCode-impl (Landroidx/compose/ui/text/LinkAnnotation;)I
41 | public fun toString ()Ljava/lang/String;
42 | public static fun toString-impl (Landroidx/compose/ui/text/LinkAnnotation;)Ljava/lang/String;
43 | public final synthetic fun unbox-impl ()Landroidx/compose/ui/text/LinkAnnotation;
44 | }
45 |
46 | public final class com/radusalagean/uitextcompose/core/UITextAnnotation$Paragraph : com/radusalagean/uitextcompose/core/UITextAnnotation {
47 | public static final synthetic fun box-impl (Landroidx/compose/ui/text/ParagraphStyle;)Lcom/radusalagean/uitextcompose/core/UITextAnnotation$Paragraph;
48 | public static fun constructor-impl (Landroidx/compose/ui/text/ParagraphStyle;)Landroidx/compose/ui/text/ParagraphStyle;
49 | public fun equals (Ljava/lang/Object;)Z
50 | public static fun equals-impl (Landroidx/compose/ui/text/ParagraphStyle;Ljava/lang/Object;)Z
51 | public static final fun equals-impl0 (Landroidx/compose/ui/text/ParagraphStyle;Landroidx/compose/ui/text/ParagraphStyle;)Z
52 | public fun hashCode ()I
53 | public static fun hashCode-impl (Landroidx/compose/ui/text/ParagraphStyle;)I
54 | public fun toString ()Ljava/lang/String;
55 | public static fun toString-impl (Landroidx/compose/ui/text/ParagraphStyle;)Ljava/lang/String;
56 | public final synthetic fun unbox-impl ()Landroidx/compose/ui/text/ParagraphStyle;
57 | }
58 |
59 | public final class com/radusalagean/uitextcompose/core/UITextAnnotation$Span : com/radusalagean/uitextcompose/core/UITextAnnotation {
60 | public static final synthetic fun box-impl (Landroidx/compose/ui/text/SpanStyle;)Lcom/radusalagean/uitextcompose/core/UITextAnnotation$Span;
61 | public static fun constructor-impl (Landroidx/compose/ui/text/SpanStyle;)Landroidx/compose/ui/text/SpanStyle;
62 | public fun equals (Ljava/lang/Object;)Z
63 | public static fun equals-impl (Landroidx/compose/ui/text/SpanStyle;Ljava/lang/Object;)Z
64 | public static final fun equals-impl0 (Landroidx/compose/ui/text/SpanStyle;Landroidx/compose/ui/text/SpanStyle;)Z
65 | public fun hashCode ()I
66 | public static fun hashCode-impl (Landroidx/compose/ui/text/SpanStyle;)I
67 | public fun toString ()Ljava/lang/String;
68 | public static fun toString-impl (Landroidx/compose/ui/text/SpanStyle;)Ljava/lang/String;
69 | public final synthetic fun unbox-impl ()Landroidx/compose/ui/text/SpanStyle;
70 | }
71 |
72 | public abstract interface class com/radusalagean/uitextcompose/core/UITextBase {
73 | public abstract fun buildAnnotatedStringComposable (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/text/AnnotatedString;
74 | public abstract fun buildStringComposable (Landroidx/compose/runtime/Composer;I)Ljava/lang/String;
75 | }
76 |
77 | public abstract interface class com/radusalagean/uitextcompose/core/UITextBuilderBase {
78 | public abstract fun build ()Ljava/lang/Object;
79 | public abstract fun raw (Ljava/lang/CharSequence;)V
80 | }
81 |
82 | public abstract interface annotation class com/radusalagean/uitextcompose/core/UITextDslMarker : java/lang/annotation/Annotation {
83 | }
84 |
85 | public final class com/radusalagean/uitextcompose/core/UITextUtil {
86 | public static final field $stable I
87 | public static final field INSTANCE Lcom/radusalagean/uitextcompose/core/UITextUtil;
88 | public final fun buildAnnotatedStringWithAndroidStringResourceRules (Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function0;)Ljava/lang/CharSequence;
89 | public final fun buildAnnotatedStringWithComposeMultiplatformStringResourceRules (Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
90 | public final fun concat (Ljava/util/List;)Ljava/lang/CharSequence;
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/uitextcompose-core/api/desktop/uitextcompose-core.api:
--------------------------------------------------------------------------------
1 | public class com/radusalagean/uitextcompose/core/AnnotationsBuilder {
2 | public static final field $stable I
3 | public fun ()V
4 | public final fun buildAnnotations ()Ljava/util/List;
5 | public final fun unaryPlus (Landroidx/compose/ui/text/LinkAnnotation;)V
6 | public final fun unaryPlus (Landroidx/compose/ui/text/ParagraphStyle;)V
7 | public final fun unaryPlus (Landroidx/compose/ui/text/SpanStyle;)V
8 | }
9 |
10 | public abstract interface annotation class com/radusalagean/uitextcompose/core/InternalApi : java/lang/annotation/Annotation {
11 | }
12 |
13 | public final class com/radusalagean/uitextcompose/core/ResBuilder : com/radusalagean/uitextcompose/core/AnnotationsBuilder {
14 | public static final field $stable I
15 | public fun ()V
16 | public final fun arg (Lcom/radusalagean/uitextcompose/core/UITextBase;Lkotlin/jvm/functions/Function1;)V
17 | public final fun arg (Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;)V
18 | public static synthetic fun arg$default (Lcom/radusalagean/uitextcompose/core/ResBuilder;Lcom/radusalagean/uitextcompose/core/UITextBase;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
19 | public static synthetic fun arg$default (Lcom/radusalagean/uitextcompose/core/ResBuilder;Ljava/lang/CharSequence;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
20 | public final fun buildResConfig ()Lcom/radusalagean/uitextcompose/core/ResConfig;
21 | }
22 |
23 | public final class com/radusalagean/uitextcompose/core/ResConfig {
24 | public static final field $stable I
25 | public fun (Ljava/util/List;Ljava/util/List;)V
26 | public final fun getAnnotations ()Ljava/util/List;
27 | public final fun getArgs ()Ljava/util/List;
28 | }
29 |
30 | public abstract interface class com/radusalagean/uitextcompose/core/UITextAnnotation {
31 | }
32 |
33 | public final class com/radusalagean/uitextcompose/core/UITextAnnotation$Link : com/radusalagean/uitextcompose/core/UITextAnnotation {
34 | public static final synthetic fun box-impl (Landroidx/compose/ui/text/LinkAnnotation;)Lcom/radusalagean/uitextcompose/core/UITextAnnotation$Link;
35 | public static fun constructor-impl (Landroidx/compose/ui/text/LinkAnnotation;)Landroidx/compose/ui/text/LinkAnnotation;
36 | public fun equals (Ljava/lang/Object;)Z
37 | public static fun equals-impl (Landroidx/compose/ui/text/LinkAnnotation;Ljava/lang/Object;)Z
38 | public static final fun equals-impl0 (Landroidx/compose/ui/text/LinkAnnotation;Landroidx/compose/ui/text/LinkAnnotation;)Z
39 | public fun hashCode ()I
40 | public static fun hashCode-impl (Landroidx/compose/ui/text/LinkAnnotation;)I
41 | public fun toString ()Ljava/lang/String;
42 | public static fun toString-impl (Landroidx/compose/ui/text/LinkAnnotation;)Ljava/lang/String;
43 | public final synthetic fun unbox-impl ()Landroidx/compose/ui/text/LinkAnnotation;
44 | }
45 |
46 | public final class com/radusalagean/uitextcompose/core/UITextAnnotation$Paragraph : com/radusalagean/uitextcompose/core/UITextAnnotation {
47 | public static final synthetic fun box-impl (Landroidx/compose/ui/text/ParagraphStyle;)Lcom/radusalagean/uitextcompose/core/UITextAnnotation$Paragraph;
48 | public static fun constructor-impl (Landroidx/compose/ui/text/ParagraphStyle;)Landroidx/compose/ui/text/ParagraphStyle;
49 | public fun equals (Ljava/lang/Object;)Z
50 | public static fun equals-impl (Landroidx/compose/ui/text/ParagraphStyle;Ljava/lang/Object;)Z
51 | public static final fun equals-impl0 (Landroidx/compose/ui/text/ParagraphStyle;Landroidx/compose/ui/text/ParagraphStyle;)Z
52 | public fun hashCode ()I
53 | public static fun hashCode-impl (Landroidx/compose/ui/text/ParagraphStyle;)I
54 | public fun toString ()Ljava/lang/String;
55 | public static fun toString-impl (Landroidx/compose/ui/text/ParagraphStyle;)Ljava/lang/String;
56 | public final synthetic fun unbox-impl ()Landroidx/compose/ui/text/ParagraphStyle;
57 | }
58 |
59 | public final class com/radusalagean/uitextcompose/core/UITextAnnotation$Span : com/radusalagean/uitextcompose/core/UITextAnnotation {
60 | public static final synthetic fun box-impl (Landroidx/compose/ui/text/SpanStyle;)Lcom/radusalagean/uitextcompose/core/UITextAnnotation$Span;
61 | public static fun constructor-impl (Landroidx/compose/ui/text/SpanStyle;)Landroidx/compose/ui/text/SpanStyle;
62 | public fun equals (Ljava/lang/Object;)Z
63 | public static fun equals-impl (Landroidx/compose/ui/text/SpanStyle;Ljava/lang/Object;)Z
64 | public static final fun equals-impl0 (Landroidx/compose/ui/text/SpanStyle;Landroidx/compose/ui/text/SpanStyle;)Z
65 | public fun hashCode ()I
66 | public static fun hashCode-impl (Landroidx/compose/ui/text/SpanStyle;)I
67 | public fun toString ()Ljava/lang/String;
68 | public static fun toString-impl (Landroidx/compose/ui/text/SpanStyle;)Ljava/lang/String;
69 | public final synthetic fun unbox-impl ()Landroidx/compose/ui/text/SpanStyle;
70 | }
71 |
72 | public abstract interface class com/radusalagean/uitextcompose/core/UITextBase {
73 | public abstract fun buildAnnotatedStringComposable (Landroidx/compose/runtime/Composer;I)Landroidx/compose/ui/text/AnnotatedString;
74 | public abstract fun buildStringComposable (Landroidx/compose/runtime/Composer;I)Ljava/lang/String;
75 | }
76 |
77 | public abstract interface class com/radusalagean/uitextcompose/core/UITextBuilderBase {
78 | public abstract fun build ()Ljava/lang/Object;
79 | public abstract fun raw (Ljava/lang/CharSequence;)V
80 | }
81 |
82 | public abstract interface annotation class com/radusalagean/uitextcompose/core/UITextDslMarker : java/lang/annotation/Annotation {
83 | }
84 |
85 | public final class com/radusalagean/uitextcompose/core/UITextUtil {
86 | public static final field $stable I
87 | public static final field INSTANCE Lcom/radusalagean/uitextcompose/core/UITextUtil;
88 | public final fun buildAnnotatedStringWithAndroidStringResourceRules (Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function0;)Ljava/lang/CharSequence;
89 | public final fun buildAnnotatedStringWithComposeMultiplatformStringResourceRules (Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
90 | public final fun concat (Ljava/util/List;)Ljava/lang/CharSequence;
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/UITextBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform
2 |
3 | import com.radusalagean.uitextcompose.core.InternalApi
4 | import com.radusalagean.uitextcompose.core.ResBuilder
5 | import com.radusalagean.uitextcompose.core.UITextBuilderBase
6 | import com.radusalagean.uitextcompose.core.UITextDslMarker
7 | import org.jetbrains.compose.resources.PluralStringResource
8 | import org.jetbrains.compose.resources.StringResource
9 |
10 | /**
11 | * Creates a new [UIText] instance using a DSL builder for multiplatform projects.
12 | *
13 | * This function provides a concise way to create text from various sources including
14 | * raw text and Compose Multiplatform string resources. It supports styling and formatting
15 | * through a type-safe DSL.
16 | *
17 | * Example:
18 | * ```
19 | * // Create from raw text
20 | * val text1 = UIText {
21 | * raw("Hello, World!")
22 | * }
23 | *
24 | * // Create from string resource
25 | * val text2 = UIText {
26 | * res(Res.string.greeting) {
27 | * arg("User")
28 | * }
29 | * }
30 | *
31 | * // Create from plural resource
32 | * val text3 = UIText {
33 | * pluralRes(Res.plurals.items_count, 5)
34 | * }
35 | *
36 | * // Combine multiple sources
37 | * val text4 = UIText {
38 | * raw("Hello, ")
39 | * res(Res.string.user_name)
40 | * raw("!")
41 | * }
42 | * ```
43 | *
44 | * @param block The builder block for configuring the text.
45 | * @return A new [UIText] instance.
46 | */
47 | public fun UIText(block: UITextBuilder.() -> Unit): UIText = UITextBuilder().apply(block).build()
48 |
49 | /**
50 | * Builder for creating [UIText] instances for multiplatform projects.
51 | *
52 | * This builder allows creating text from multiple sources including raw text and
53 | * Compose Multiplatform string resources. It supports styling and formatting through annotations.
54 | *
55 | * The builder collects text components and combines them into a single [UIText] instance.
56 | *
57 | * @see UIText
58 | * @see raw
59 | * @see res
60 | * @see pluralRes
61 | */
62 | @UITextDslMarker
63 | @OptIn(InternalApi::class)
64 | public class UITextBuilder : UITextBuilderBase {
65 | private val components = mutableListOf()
66 |
67 | /**
68 | * Adds raw text content to the builder.
69 | *
70 | * Use this method to add plain text that doesn't come from a resource.
71 | *
72 | * Example:
73 | * ```
74 | * UIText {
75 | * raw("Hello, World!")
76 | * }
77 | * ```
78 | *
79 | * @param text The text content to add.
80 | */
81 | override fun raw(text: CharSequence) {
82 | components += UIText.Raw(text)
83 | }
84 |
85 | /**
86 | * Adds text from a Compose Multiplatform string resource.
87 | *
88 | * This method resolves text from a Compose Multiplatform string resource and allows adding
89 | * arguments for placeholders in the resource string.
90 | *
91 | * Example:
92 | * ```
93 | * UIText {
94 | * res(Res.string.greeting) {
95 | * arg("User") {
96 | * +SpanStyle(color = Color.Blue)
97 | * }
98 | * }
99 | * }
100 | * ```
101 | *
102 | * @param stringResource The Compose Multiplatform string resource to use.
103 | * @param resBuilder Optional builder for configuring arguments and styling.
104 | */
105 | public fun res(
106 | stringResource: StringResource,
107 | resBuilder: ResBuilder.() -> Unit = { }
108 | ) {
109 | val config = ResBuilder().apply(resBuilder).buildResConfig()
110 | components += UIText.Res(
111 | stringResource = stringResource,
112 | args = config.args,
113 | baseAnnotations = config.annotations
114 | )
115 | }
116 |
117 | /**
118 | * Adds text from a Compose Multiplatform plural resource.
119 | *
120 | * This method resolves text from a Compose Multiplatform plural resource based on the given quantity
121 | * and allows adding arguments for placeholders in the resource string.
122 | *
123 | * By default, the quantity is automatically added as the first argument.
124 | *
125 | * Example:
126 | * ```
127 | * UIText {
128 | * pluralRes(Res.plurals.items_count, 5) {
129 | * // Additional arguments if needed
130 | * arg("category") {
131 | * +SpanStyle(fontWeight = FontWeight.Bold)
132 | * }
133 | * }
134 | * }
135 | * ```
136 | *
137 | * @param pluralStringResource The Compose Multiplatform plural resource to use.
138 | * @param quantity The quantity value for selecting the appropriate plural form.
139 | * @param resBuilder Optional builder for configuring arguments and styling.
140 | */
141 | public fun pluralRes(
142 | pluralStringResource: PluralStringResource,
143 | quantity: Int,
144 | resBuilder: ResBuilder.() -> Unit = {
145 | arg(quantity.toString())
146 | }
147 | ) {
148 | val config = ResBuilder().apply(resBuilder).buildResConfig()
149 | components += UIText.PluralRes(
150 | pluralStringResource = pluralStringResource,
151 | quantity = quantity,
152 | args = config.args,
153 | baseAnnotations = config.annotations
154 | )
155 | }
156 |
157 | /**
158 | * Builds and returns the final [UIText] instance.
159 | *
160 | * This method creates an appropriate [UIText] instance based on the components
161 | * added to the builder:
162 | * - If no components were added, returns an empty text.
163 | * - If only one component was added, returns that component directly.
164 | * - If multiple components were added, returns a compound text that combines them.
165 | *
166 | * @return The built [UIText] instance.
167 | */
168 | override fun build(): UIText = when (components.size) {
169 | 0 -> UIText.Raw("")
170 | 1 -> components[0]
171 | else -> UIText.Compound(components)
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/androidMain/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
--------------------------------------------------------------------------------
/uitextcompose-core/src/commonMain/kotlin/com/radusalagean/uitextcompose/core/UITextBuilderBase.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.core
2 |
3 | import androidx.compose.ui.text.LinkAnnotation
4 | import androidx.compose.ui.text.ParagraphStyle
5 | import androidx.compose.ui.text.SpanStyle
6 |
7 | @InternalApi
8 | @DslMarker
9 | public annotation class UITextDslMarker
10 |
11 | /**
12 | * Base interface for building text representations in a type-safe DSL.
13 | *
14 | * This interface provides a foundation for DSL builders that create text representations
15 | * for Compose UI. Implementations can add text from various sources and build
16 | * the final result with appropriate styling.
17 | *
18 | * @param T The type of the result built by this builder.
19 | *
20 | * @see raw
21 | * @see build
22 | */
23 | @OptIn(InternalApi::class)
24 | @UITextDslMarker
25 | public interface UITextBuilderBase {
26 | /**
27 | * Adds raw text content to the builder.
28 | *
29 | * Use this method to add plain text that doesn't come from a resource.
30 | *
31 | * Example:
32 | * ```
33 | * UITextBuilder().apply {
34 | * raw("Hello, World!")
35 | * }.build()
36 | * ```
37 | *
38 | * @param text The text content to add.
39 | */
40 | public fun raw(text: CharSequence)
41 |
42 | /**
43 | * Builds and returns the final text representation.
44 | *
45 | * Call this method after adding all the needed content to get the resulting text object.
46 | *
47 | * @return The built text representation of type [T].
48 | */
49 | public fun build(): T
50 | }
51 |
52 | /**
53 | * Builder for configuring string resource arguments and annotations.
54 | *
55 | * This builder is used to add arguments to string resources and apply text styling.
56 | * It supports both simple text arguments and nested [UITextBase] objects as arguments.
57 | *
58 | * Example:
59 | * ```
60 | * UITextBuilder().apply {
61 | * res(R.string.greeting) {
62 | * arg("World") {
63 | * +SpanStyle(color = Color.Blue)
64 | * }
65 | * }
66 | * }.build()
67 | * ```
68 | */
69 | @OptIn(InternalApi::class)
70 | @UITextDslMarker
71 | public class ResBuilder : AnnotationsBuilder() {
72 | private val args = mutableListOf>>()
73 |
74 | /**
75 | * Adds a text argument to the string resource.
76 | *
77 | * Use this method to provide arguments for string resource placeholders.
78 | * You can optionally apply styling to the argument.
79 | *
80 | * @param value The text value to use as an argument.
81 | * @param annotationsBuilder Optional lambda to apply styling to this argument.
82 | */
83 | public fun arg(
84 | value: CharSequence,
85 | annotationsBuilder: AnnotationsBuilder.() -> Unit = { }
86 | ) {
87 | val builder = AnnotationsBuilder().apply(annotationsBuilder)
88 | args += value to builder.buildAnnotations()
89 | }
90 |
91 | /**
92 | * Adds a [UITextBase] argument to the string resource.
93 | *
94 | * Use this method to provide nested text objects as arguments for string resource placeholders.
95 | * You can optionally apply additional styling to the argument.
96 | *
97 | * @param value The [UITextBase] object to use as an argument.
98 | * @param annotationsBuilder Optional lambda to apply styling to this argument.
99 | */
100 | public fun arg(
101 | value: T,
102 | annotationsBuilder: AnnotationsBuilder.() -> Unit = { }
103 | ) {
104 | val builder = AnnotationsBuilder().apply(annotationsBuilder)
105 | args += value to builder.buildAnnotations()
106 | }
107 |
108 | /**
109 | * Builds the resource configuration with all arguments and annotations.
110 | *
111 | * This is called internally by the library to create the configuration needed
112 | * to build the final text representation.
113 | *
114 | * @return A [ResConfig] object containing all arguments and annotations.
115 | */
116 | public fun buildResConfig(): ResConfig = ResConfig(
117 | annotations = annotations,
118 | args = args
119 | )
120 | }
121 |
122 | @InternalApi
123 | public class ResConfig(
124 | public val annotations: List,
125 | public val args: List>>
126 | )
127 |
128 | /**
129 | * Builder for text annotations like styling and links.
130 | *
131 | * This builder allows adding styling information to text using a DSL syntax.
132 | * It supports span styles, paragraph styles, and link annotations.
133 | *
134 | * Example:
135 | * ```
136 | * AnnotationsBuilder().apply {
137 | * +SpanStyle(color = Color.Red, fontWeight = FontWeight.Bold)
138 | * +ParagraphStyle(textAlign = TextAlign.Center)
139 | * +LinkAnnotation.Url(url = "https://example.com")
140 | * }
141 | * ```
142 | */
143 | @OptIn(InternalApi::class)
144 | @UITextDslMarker
145 | public open class AnnotationsBuilder {
146 | internal val annotations: MutableList = mutableListOf()
147 |
148 | /**
149 | * Adds a span style to the text.
150 | *
151 | * Use the unary plus operator to add styling like color, font weight, or text decoration.
152 | *
153 | * Example:
154 | * ```
155 | * +SpanStyle(color = Color.Red, fontWeight = FontWeight.Bold)
156 | * ```
157 | *
158 | * @param SpanStyle The span style to add.
159 | */
160 | public operator fun SpanStyle.unaryPlus() {
161 | annotations.add(UITextAnnotation.Span(this))
162 | }
163 |
164 | /**
165 | * Adds a paragraph style to the text.
166 | *
167 | * Use the unary plus operator to add paragraph styling like alignment or indentation.
168 | *
169 | * Example:
170 | * ```
171 | * +ParagraphStyle(textAlign = TextAlign.Center)
172 | * ```
173 | *
174 | * @param ParagraphStyle The paragraph style to add.
175 | */
176 | public operator fun ParagraphStyle.unaryPlus() {
177 | annotations.add(UITextAnnotation.Paragraph(this))
178 | }
179 |
180 | /**
181 | * Adds a link annotation to the text.
182 | *
183 | * Use the unary plus operator to add a clickable link to the text.
184 | *
185 | * Example:
186 | * ```
187 | * +LinkAnnotation.Url(url = "https://example.com")
188 | * ```
189 | *
190 | * @param LinkAnnotation The link annotation to add.
191 | */
192 | public operator fun LinkAnnotation.unaryPlus() {
193 | annotations.add(UITextAnnotation.Link(this))
194 | }
195 |
196 | /**
197 | * Builds and returns all annotations added to this builder.
198 | *
199 | * This is called internally by the library to retrieve the annotations.
200 | *
201 | * @return A list of [UITextAnnotation] objects representing the added annotations.
202 | */
203 | public fun buildAnnotations(): List = annotations
204 | }
--------------------------------------------------------------------------------
/uitextcompose-core/src/commonMain/kotlin/com/radusalagean/uitextcompose/core/UITextUtil.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.core
2 |
3 | import androidx.compose.ui.text.AnnotatedString
4 | import androidx.compose.ui.text.withLink
5 | import androidx.compose.ui.text.withStyle
6 |
7 | @InternalApi
8 | public object UITextUtil {
9 | // Regex to match both numbered (%1$s) and unnumbered (%s) placeholders,
10 | // excluding escaped ones
11 | private val placeholderRegexAndroidStringResources: Regex =
12 | Regex("(? append(arg)
24 | else -> append(arg.toString())
25 | }
26 | }
27 |
28 | private fun AnnotatedString.Builder.handleUITextAnnotations(
29 | uiTextAnnotations: List,
30 | block: () -> Unit
31 | ) {
32 | if (uiTextAnnotations.isEmpty()) {
33 | block()
34 | return
35 | }
36 |
37 | fun applyAnnotation(index: Int) {
38 | if (index >= uiTextAnnotations.size) {
39 | block()
40 | return
41 | }
42 |
43 | when (val annotation = uiTextAnnotations[index]) {
44 | is UITextAnnotation.Span -> {
45 | withStyle(annotation.spanStyle) {
46 | applyAnnotation(index + 1)
47 | }
48 | }
49 | is UITextAnnotation.Paragraph -> {
50 | withStyle(annotation.paragraphStyle) {
51 | applyAnnotation(index + 1)
52 | }
53 | }
54 | is UITextAnnotation.Link -> {
55 | withLink(annotation.linkAnnotation) {
56 | applyAnnotation(index + 1)
57 | }
58 | }
59 | }
60 | }
61 |
62 | applyAnnotation(0)
63 | }
64 |
65 | /**
66 | * Builds an annotated string using a synchronous string provider
67 | * Used by the implementation for Android String Resources
68 | *
69 | * For more details, see the [API documentation](https://developer.android.com/guide/topics/resources/string-resource).
70 | */
71 | public fun buildAnnotatedStringWithAndroidStringResourceRules(
72 | resolvedArgs: List>>,
73 | baseAnnotations: List,
74 | baseStringProvider: () -> String
75 | ): CharSequence {
76 | return buildAnnotatedStringInternal(
77 | resolvedArgs = resolvedArgs,
78 | baseAnnotations = baseAnnotations,
79 | baseString = baseStringProvider(),
80 | placeholderRegex = placeholderRegexAndroidStringResources,
81 | removeEscapedPlaceholderCharacter = true
82 | )
83 | }
84 |
85 | /**
86 | * Builds an annotated string using a suspend string provider
87 | * Used by the implementation for Compose Multiplatform String Resources
88 | *
89 | * For more details, see the [API documentation](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-resources-usage.html#strings).
90 | */
91 | public suspend fun buildAnnotatedStringWithComposeMultiplatformStringResourceRules(
92 | resolvedArgs: List>>,
93 | baseAnnotations: List,
94 | baseStringProvider: suspend () -> String
95 | ): CharSequence {
96 | return buildAnnotatedStringInternal(
97 | resolvedArgs = resolvedArgs,
98 | baseAnnotations = baseAnnotations,
99 | baseString = baseStringProvider(),
100 | placeholderRegex = placeholderRegexComposeMultiplatformStringResources,
101 | removeEscapedPlaceholderCharacter = false
102 | )
103 | }
104 |
105 | /**
106 | * Internal implementation that both sync and suspend functions delegate to
107 | */
108 | private fun buildAnnotatedStringInternal(
109 | resolvedArgs: List>>,
110 | baseAnnotations: List,
111 | baseString: String,
112 | placeholderRegex: Regex,
113 | removeEscapedPlaceholderCharacter: Boolean
114 | ): CharSequence {
115 | return androidx.compose.ui.text.buildAnnotatedString {
116 | var unnumberedCount = 0
117 | val placeholders = placeholderRegex.findAll(baseString).map {
118 | val group = it.groups[1]?.value
119 | if (group != null) {
120 | // For numbered placeholders (%1$s), extract the number
121 | group.removeSuffix("$").toInt() - 1
122 | } else {
123 | // For unnumbered placeholders (%s), use sequential count
124 | unnumberedCount++
125 | unnumberedCount - 1
126 | }
127 | }.toList()
128 |
129 | var parts = baseString.split(placeholderRegex)
130 | if (removeEscapedPlaceholderCharacter) {
131 | // Remove leading % from escaped placeholders in each part
132 | parts = parts.map { part ->
133 | part.replace(escapedPlaceholderRegex) { matchResult ->
134 | matchResult.value.substring(1)
135 | }
136 | }
137 | }
138 | handleUITextAnnotations(baseAnnotations) {
139 | parts.forEachIndexed { index, part ->
140 | append(part)
141 | if (index !in placeholders.indices)
142 | return@handleUITextAnnotations
143 | val placeholderIndex = placeholders[index]
144 | val uiTextAnnotations = resolvedArgs[placeholderIndex].second
145 | val arg = resolvedArgs[placeholderIndex].first
146 | handleUITextAnnotations(uiTextAnnotations) {
147 | appendAny(arg)
148 | }
149 | }
150 | }
151 | }
152 | }
153 |
154 | /**
155 | * Utility to concatenate CharSequences with style preservation
156 | */
157 | public fun concat(parts: List): CharSequence {
158 | if (parts.isEmpty())
159 | return ""
160 |
161 | if (parts.size == 1)
162 | return parts[0]
163 |
164 | val annotated = parts.any { it is AnnotatedString }
165 | return if (annotated) {
166 | androidx.compose.ui.text.buildAnnotatedString {
167 | parts.forEach {
168 | append(it)
169 | }
170 | }
171 | } else {
172 | buildString {
173 | parts.forEach {
174 | append(it)
175 | }
176 | }
177 | }
178 | }
179 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
147 | # shellcheck disable=SC3045
148 | MAX_FD=$( ulimit -H -n ) ||
149 | warn "Could not query maximum file descriptor limit"
150 | esac
151 | case $MAX_FD in #(
152 | '' | soft) :;; #(
153 | *)
154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
155 | # shellcheck disable=SC3045
156 | ulimit -n "$MAX_FD" ||
157 | warn "Could not set maximum file descriptor limit to $MAX_FD"
158 | esac
159 | fi
160 |
161 | # Collect all arguments for the java command, stacking in reverse order:
162 | # * args from the command line
163 | # * the main class name
164 | # * -classpath
165 | # * -D...appname settings
166 | # * --module-path (only if needed)
167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
168 |
169 | # For Cygwin or MSYS, switch paths to Windows format before running java
170 | if "$cygwin" || "$msys" ; then
171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
173 |
174 | JAVACMD=$( cygpath --unix "$JAVACMD" )
175 |
176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
177 | for arg do
178 | if
179 | case $arg in #(
180 | -*) false ;; # don't mess with options #(
181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
182 | [ -e "$t" ] ;; #(
183 | *) false ;;
184 | esac
185 | then
186 | arg=$( cygpath --path --ignore --mixed "$arg" )
187 | fi
188 | # Roll the args list around exactly as many times as the number of
189 | # args, so each arg winds up back in the position where it started, but
190 | # possibly modified.
191 | #
192 | # NB: a `for` loop captures its iteration list before it begins, so
193 | # changing the positional parameters here affects neither the number of
194 | # iterations, nor the values presented in `arg`.
195 | shift # remove old arg
196 | set -- "$@" "$arg" # push replacement arg
197 | done
198 | fi
199 |
200 | # Collect all arguments for the java command;
201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
202 | # shell script including quotes and variable substitutions, so put them in
203 | # double quotes to make sure that they get re-expanded; and
204 | # * put everything else in single quotes, so that it's not re-expanded.
205 |
206 | set -- \
207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
208 | -classpath "$CLASSPATH" \
209 | org.gradle.wrapper.GradleWrapperMain \
210 | "$@"
211 |
212 | # Stop when "xargs" is not available.
213 | if ! command -v xargs >/dev/null 2>&1
214 | then
215 | die "xargs is not available"
216 | fi
217 |
218 | # Use "xargs" to parse quoted args.
219 | #
220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
221 | #
222 | # In Bash we could simply go:
223 | #
224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
225 | # set -- "${ARGS[@]}" "$@"
226 | #
227 | # but POSIX shell has neither arrays nor command substitution, so instead we
228 | # post-process each arg (as a line of input to sed) to backslash-escape any
229 | # character that might be a shell metacharacter, then use eval to reverse
230 | # that process (while maintaining the separation between arguments), and wrap
231 | # the whole thing up as a single "set" statement.
232 | #
233 | # This will of course break if any of these variables contains a newline or
234 | # an unmatched quote.
235 | #
236 |
237 | eval "set -- $(
238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
239 | xargs -n1 |
240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
241 | tr '\n' ' '
242 | )" '"$@"'
243 |
244 | exec "$JAVACMD" "$@"
245 |
--------------------------------------------------------------------------------
/uitextcompose-android/src/androidMain/kotlin/com/radusalagean/uitextcompose/android/UIText.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android
2 |
3 | import android.content.Context
4 | import androidx.annotation.PluralsRes
5 | import androidx.annotation.StringRes
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.platform.LocalConfiguration
9 | import androidx.compose.ui.platform.LocalContext
10 | import androidx.compose.ui.text.AnnotatedString
11 | import com.radusalagean.uitextcompose.core.InternalApi
12 | import com.radusalagean.uitextcompose.core.UITextAnnotation
13 | import com.radusalagean.uitextcompose.core.UITextBase
14 | import com.radusalagean.uitextcompose.core.UITextUtil
15 |
16 | /**
17 | * Implementation of [UITextBase] for handling text in Android applications
18 | * using Android String Resources.
19 | *
20 | * This class provides a way to work with text from various sources such as:
21 | * - Raw text strings
22 | * - Android string resources
23 | * - Android plural resources
24 | *
25 | * It handles string formatting with placeholders and supports styling through span styles,
26 | * paragraph styles, and link annotations.
27 | *
28 | * Example usage:
29 | * ```
30 | * // Create a UIText instance using the DSL
31 | * val text = UIText {
32 | * res(R.string.greeting) {
33 | * arg("User") {
34 | * +SpanStyle(color = Color.Blue)
35 | * }
36 | * }
37 | * }
38 | *
39 | * // Use it in a Composable
40 | * @Composable
41 | * fun Greeting(text: UIText) {
42 | * Text(text = text.buildAnnotatedStringComposable())
43 | * }
44 | * ```
45 | *
46 | * @see UITextBuilder
47 | * @see UITextBase
48 | */
49 | @OptIn(InternalApi::class)
50 | public sealed class UIText : UITextBase {
51 |
52 | protected abstract fun build(context: Context): CharSequence
53 |
54 | @Composable
55 | private fun rememberResourceState(
56 | block: () -> T
57 | ): T {
58 | val configuration = LocalConfiguration.current
59 | return remember(configuration) {
60 | block()
61 | }
62 | }
63 |
64 | /**
65 | * Builds a plain string representation of the text using the provided context.
66 | *
67 | * This method resolves the text content from its source (raw, resource, etc.) and
68 | * returns it as a plain string. Any styling information will be lost.
69 | *
70 | * @param context The Android context to use for resolving resources.
71 | * @return A plain string representation of the text content.
72 | */
73 | public fun buildString(context: Context): String {
74 | return when (val charSequence = build(context)) {
75 | is String -> charSequence
76 | is AnnotatedString -> charSequence.toString() // We drop any style here
77 | else -> ""
78 | }
79 | }
80 |
81 | /**
82 | * Builds a plain string representation of the text in a composable context.
83 | *
84 | * This composable function retrieves the current Android context and configuration,
85 | * then resolves the text content from its source and returns it as a plain string.
86 | * The result is remembered based on configuration changes.
87 | *
88 | * @return A plain string representation of the text content.
89 | */
90 | @Composable
91 | public override fun buildStringComposable(): String {
92 | val context = LocalContext.current
93 | return rememberResourceState {
94 | buildString(context)
95 | }
96 | }
97 |
98 | /**
99 | * Builds an annotated string representation of the text using the provided context.
100 | *
101 | * This method resolves the text content from its source (raw, resource, etc.) and
102 | * returns it as an [AnnotatedString] that preserves styling information.
103 | *
104 | * @param context The Android context to use for resolving resources.
105 | * @return An [AnnotatedString] representation of the text content with styling.
106 | */
107 | public fun buildAnnotatedString(context: Context): AnnotatedString {
108 | return when (val charSequence = build(context)) {
109 | is String -> AnnotatedString(charSequence)
110 | is AnnotatedString -> charSequence
111 | else -> AnnotatedString("")
112 | }
113 | }
114 |
115 | /**
116 | * Builds an annotated string representation of the text in a composable context.
117 | *
118 | * This composable function retrieves the current Android context and configuration,
119 | * then resolves the text content from its source and returns it as an [AnnotatedString] with styling.
120 | * The result is remembered based on configuration changes.
121 | *
122 | * @return An [AnnotatedString] representation of the text content with styling.
123 | */
124 | @Composable
125 | public override fun buildAnnotatedStringComposable(): AnnotatedString {
126 | val context = LocalContext.current
127 | return rememberResourceState {
128 | buildAnnotatedString(context)
129 | }
130 | }
131 |
132 | protected fun resolveArg(context: Context, arg: Any): Any = when (arg) {
133 | is UIText -> arg.build(context)
134 | else -> arg
135 | }
136 |
137 | protected fun hasAnnotations(
138 | args: List>>,
139 | resolvedArgs: List>>,
140 | baseAnnotations: List
141 | ): Boolean {
142 | if (baseAnnotations.isNotEmpty()) return true
143 | if (args.any { it.second.isNotEmpty() }) return true
144 | return resolvedArgs.any { it.first is AnnotatedString }
145 | }
146 |
147 | internal class Raw(private val text: CharSequence) : UIText() {
148 | override fun build(context: Context): CharSequence {
149 | return text
150 | }
151 | }
152 |
153 | internal class Res(
154 | @StringRes private val resId: Int,
155 | private val args: List>>,
156 | private val baseAnnotations: List
157 | ) : UIText() {
158 |
159 | override fun build(context: Context): CharSequence {
160 | if (args.isEmpty() && baseAnnotations.isEmpty()) {
161 | return context.getString(resId)
162 | }
163 | val resolvedArgs = args.map {
164 | resolveArg(context, it.first) to it.second
165 | }
166 | val annotated = hasAnnotations(
167 | args = args,
168 | resolvedArgs = resolvedArgs,
169 | baseAnnotations = baseAnnotations
170 | )
171 | return if (annotated) {
172 | UITextUtil.buildAnnotatedStringWithAndroidStringResourceRules(
173 | resolvedArgs = resolvedArgs,
174 | baseAnnotations = baseAnnotations,
175 | baseStringProvider = {
176 | context.getString(resId)
177 | }
178 | )
179 | } else if (resolvedArgs.isNotEmpty()) {
180 | val argValues = Array(resolvedArgs.size) { i ->
181 | resolvedArgs[i].first
182 | }
183 | context.getString(resId, *argValues)
184 | } else {
185 | context.getString(resId)
186 | }
187 | }
188 | }
189 |
190 | internal class PluralRes(
191 | @PluralsRes private val resId: Int,
192 | private val quantity: Int,
193 | private val args: List>>,
194 | private val baseAnnotations: List
195 | ) : UIText() {
196 |
197 | override fun build(context: Context): CharSequence {
198 | if (args.isEmpty() && baseAnnotations.isEmpty()) {
199 | return context.resources.getQuantityString(resId, quantity)
200 | }
201 | val resolvedArgs = args.map {
202 | resolveArg(context, it.first) to it.second
203 | }
204 | val annotated = hasAnnotations(
205 | args = args,
206 | resolvedArgs = resolvedArgs,
207 | baseAnnotations = baseAnnotations
208 | )
209 | return if (annotated) {
210 | UITextUtil.buildAnnotatedStringWithAndroidStringResourceRules(
211 | resolvedArgs = resolvedArgs,
212 | baseAnnotations = baseAnnotations,
213 | baseStringProvider = {
214 | context.resources.getQuantityString(resId, quantity)
215 | }
216 | )
217 | } else if (resolvedArgs.isNotEmpty()) {
218 | val argValues = Array(resolvedArgs.size) { i ->
219 | resolvedArgs[i].first
220 | }
221 | context.resources.getQuantityString(resId, quantity, *argValues)
222 | } else {
223 | context.resources.getQuantityString(resId, quantity)
224 | }
225 | }
226 | }
227 |
228 | internal class Compound(
229 | private val components: List
230 | ) : UIText() {
231 | override fun build(context: Context): CharSequence {
232 | if (components.isEmpty()) {
233 | return ""
234 | }
235 | if (components.size == 1) {
236 | return components[0].build(context)
237 | }
238 | val resolvedComponents = components.map { it.build(context) }
239 | return UITextUtil.concat(resolvedComponents)
240 | }
241 | }
242 | }
--------------------------------------------------------------------------------
/uitextcompose-android-sample/src/main/java/com/radusalagean/uitextcompose/android/sample/ui/screen/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.android.sample.ui.screen
2 |
3 | import androidx.appcompat.app.AppCompatDelegate
4 | import androidx.compose.runtime.derivedStateOf
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.setValue
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.text.LinkAnnotation
10 | import androidx.compose.ui.text.ParagraphStyle
11 | import androidx.compose.ui.text.SpanStyle
12 | import androidx.compose.ui.text.TextLinkStyles
13 | import androidx.compose.ui.text.font.FontWeight
14 | import androidx.compose.ui.text.style.TextDecoration
15 | import androidx.core.os.LocaleListCompat
16 | import androidx.lifecycle.ViewModel
17 | import com.radusalagean.uitextcompose.android.UIText
18 | import com.radusalagean.uitextcompose.android.sample.R
19 | import com.radusalagean.uitextcompose.android.sample.ui.component.ExampleEntryModel
20 | import com.radusalagean.uitextcompose.android.sample.ui.component.LanguageOption
21 | import com.radusalagean.uitextcompose.android.sample.ui.theme.CustomGreen
22 |
23 | class MainViewModel : ViewModel() {
24 |
25 | // Section: Language
26 | val languageSectionTitle = UIText { res(R.string.section_title_language) }
27 | val languageOptions = listOf(
28 | LanguageOption(
29 | uiText = UIText { res(R.string.language_english) },
30 | languageCode = "en"
31 | ),
32 | LanguageOption(
33 | uiText = UIText { res(R.string.language_romanian) },
34 | languageCode = "ro"
35 | )
36 | )
37 | var selectedLanguageCode by mutableStateOf("")
38 | val selectedLanguageIndex: Int by derivedStateOf {
39 | languageOptions.indexOfFirst { it.languageCode == selectedLanguageCode }
40 | }
41 |
42 | fun syncSelectedLanguage() {
43 | val locales = AppCompatDelegate.getApplicationLocales()
44 | selectedLanguageCode = locales.get(0)?.language ?: "en"
45 | }
46 |
47 | fun onLanguageSelected(code: String) {
48 | val localesList = LocaleListCompat.forLanguageTags(code)
49 | AppCompatDelegate.setApplicationLocales(localesList)
50 | }
51 |
52 | // Section: Examples
53 | val examplesSectionTitle = UIText { res(R.string.section_title_examples) }
54 | val exampleEntries = listOf(
55 | ExampleEntryModel(
56 | label = "raw",
57 | value = UIText {
58 | raw("Radu")
59 | }
60 | ),
61 | ExampleEntryModel(
62 | label = "res",
63 | value = UIText {
64 | res(R.string.greeting) {
65 | arg("Radu")
66 | }
67 | }
68 | ),
69 | ExampleEntryModel(
70 | label = "pluralRes",
71 | value = UIText {
72 | pluralRes(R.plurals.products, 30)
73 | }
74 | ),
75 | ExampleEntryModel(
76 | label = "res - annotated",
77 | value = UIText {
78 | res(R.string.shopping_cart_status) {
79 | arg(
80 | UIText {
81 | pluralRes(R.plurals.products, 30)
82 | }
83 | )
84 | arg(
85 | UIText {
86 | res(R.string.shopping_cart_status_insert_shopping_cart) {
87 | +SpanStyle(color = Color.Red)
88 | }
89 | }
90 | )
91 | }
92 | }
93 | ),
94 | ExampleEntryModel(
95 | label = "pluralRes - annotated",
96 | value = UIText {
97 | pluralRes(R.plurals.products, 30) {
98 | arg(30.toString()) {
99 | +SpanStyle(color = CustomGreen)
100 | }
101 | +SpanStyle(fontWeight = FontWeight.Bold)
102 | }
103 | }
104 | ),
105 | ExampleEntryModel(
106 | label = "compound - example 1",
107 | value = UIText {
108 | res(R.string.greeting) {
109 | arg("Radu")
110 | }
111 | raw(" ")
112 | res(R.string.shopping_cart_status) {
113 | arg(
114 | UIText {
115 | pluralRes(R.plurals.products, 30) {
116 | arg(30.toString()) {
117 | +SpanStyle(color = CustomGreen)
118 | }
119 | +SpanStyle(fontWeight = FontWeight.Bold)
120 | }
121 | }
122 | )
123 | arg(
124 | UIText {
125 | res(R.string.shopping_cart_status_insert_shopping_cart) {
126 | +SpanStyle(color = Color.Red)
127 | }
128 | }
129 | )
130 | }
131 | }
132 | ),
133 | ExampleEntryModel(
134 | label = "compound - example 2",
135 | value = UIText {
136 | res(R.string.greeting) {
137 | arg("Radu")
138 | }
139 | raw(" ")
140 | res(R.string.shopping_cart_status) {
141 | arg(
142 | UIText {
143 | pluralRes(R.plurals.products, 30) {
144 | arg(30.toString()) {
145 | +SpanStyle(color = CustomGreen)
146 | }
147 | +SpanStyle(fontWeight = FontWeight.Bold)
148 | }
149 | }
150 | )
151 | arg(
152 | UIText {
153 | res(R.string.shopping_cart_status_insert_shopping_cart)
154 | }
155 | ) {
156 | +SpanStyle(color = Color.Red)
157 | }
158 | }
159 | raw(" ")
160 | res(R.string.proceed_to_checkout) {
161 | +LinkAnnotation.Url(
162 | url = "https://example.com",
163 | styles = TextLinkStyles(
164 | style = SpanStyle(
165 | color = Color.Blue,
166 | textDecoration = TextDecoration.Underline
167 | )
168 | )
169 | )
170 | }
171 | }
172 | ),
173 | ExampleEntryModel(
174 | label = "compound - example 3",
175 | value = UIText {
176 | res(R.string.greeting) {
177 | arg("Radu")
178 | }
179 | res(R.string.shopping_cart_status) {
180 | +ParagraphStyle()
181 | arg(
182 | UIText {
183 | pluralRes(R.plurals.products, 30) {
184 | +SpanStyle(fontWeight = FontWeight.Bold)
185 | arg(30.toString()) {
186 | +SpanStyle(color = CustomGreen)
187 | }
188 | }
189 | }
190 | )
191 | arg(
192 | UIText {
193 | res(R.string.shopping_cart_status_insert_shopping_cart)
194 | }
195 | ) {
196 | +SpanStyle(color = Color.Red)
197 | }
198 | }
199 | res(R.string.proceed_to_checkout) {
200 | +LinkAnnotation.Url(
201 | url = "https://example.com",
202 | styles = TextLinkStyles(
203 | style = SpanStyle(
204 | color = Color.Blue,
205 | textDecoration = TextDecoration.Underline
206 | )
207 | )
208 | )
209 | }
210 | }
211 | ),
212 | ExampleEntryModel(
213 | label = "terms of service & privacy policy",
214 | value = UIText {
215 | val linkStyle = TextLinkStyles(
216 | SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)
217 | )
218 | res(R.string.legal_footer_example) {
219 | arg(
220 | UIText {
221 | res(R.string.legal_footer_example_insert_terms_of_service)
222 | }
223 | ) {
224 | +LinkAnnotation.Url(
225 | url = "https://radusalagean.com/example-terms-of-service/",
226 | styles = linkStyle
227 | )
228 | }
229 | arg(
230 | UIText {
231 | res(R.string.legal_footer_example_insert_privacy_policy)
232 | }
233 | ) {
234 | +LinkAnnotation.Url(
235 | url = "https://radusalagean.com/example-privacy-policy/",
236 | styles = linkStyle
237 | )
238 | }
239 | }
240 | }
241 | )
242 | )
243 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/UIText.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.rememberCoroutineScope
9 | import androidx.compose.ui.text.AnnotatedString
10 | import com.radusalagean.uitextcompose.core.InternalApi
11 | import com.radusalagean.uitextcompose.core.UITextAnnotation
12 | import com.radusalagean.uitextcompose.core.UITextBase
13 | import com.radusalagean.uitextcompose.core.UITextUtil
14 | import kotlinx.coroutines.CoroutineStart
15 | import kotlinx.coroutines.launch
16 | import org.jetbrains.compose.resources.PluralStringResource
17 | import org.jetbrains.compose.resources.StringResource
18 | import org.jetbrains.compose.resources.getPluralString
19 | import org.jetbrains.compose.resources.getString
20 | import org.jetbrains.compose.resources.rememberResourceEnvironment
21 |
22 | /**
23 | * Implementation of [UITextBase] for handling text in Compose Multiplatform applications
24 | * using multiplatform string resources.
25 | *
26 | * This class provides a way to work with text from various sources such as:
27 | * - Raw text strings
28 | * - Compose Multiplatform string resources
29 | * - Compose Multiplatform plural resources
30 | *
31 | * It handles string formatting with placeholders and supports styling through span styles,
32 | * paragraph styles, and link annotations. This implementation works across all platforms
33 | * supported by Compose Multiplatform.
34 | *
35 | * Example usage:
36 | * ```
37 | * // Create a UIText instance using the DSL
38 | * val text = UIText {
39 | * res(Res.string.greeting) {
40 | * arg("User") {
41 | * +SpanStyle(color = Color.Blue)
42 | * }
43 | * }
44 | * }
45 | *
46 | * // Use it in a Composable
47 | * @Composable
48 | * fun Greeting(text: UIText) {
49 | * Text(text = text.buildAnnotatedStringComposable())
50 | * }
51 | * ```
52 | *
53 | * @see UITextBuilder
54 | * @see UITextBase
55 | */
56 | @OptIn(InternalApi::class)
57 | public sealed class UIText : UITextBase {
58 |
59 | protected abstract suspend fun build(): CharSequence
60 |
61 | @Composable
62 | private fun rememberResourceState(
63 | getDefault: () -> T,
64 | block: suspend () -> T
65 | ): State {
66 | val scope = rememberCoroutineScope()
67 | val resourceEnvironment = rememberResourceEnvironment()
68 | return remember(resourceEnvironment) {
69 | val mutableState = mutableStateOf(getDefault())
70 | scope.launch(start = CoroutineStart.UNDISPATCHED) {
71 | mutableState.value = block()
72 | }
73 | mutableState
74 | }
75 | }
76 |
77 | /**
78 | * Builds a plain string representation of the text.
79 | *
80 | * This suspending function resolves the text content from its source (raw, resource, etc.)
81 | * and returns it as a plain string. Any styling information will be lost.
82 | *
83 | * @return A plain string representation of the text content.
84 | */
85 | public suspend fun buildString(): String {
86 | return when (val charSequence = build()) {
87 | is String -> charSequence
88 | is AnnotatedString -> charSequence.toString() // We drop any style here
89 | else -> ""
90 | }
91 | }
92 |
93 | /**
94 | * Builds a plain string representation of the text in a composable context.
95 | *
96 | * This composable function resolves the text content from its source and returns
97 | * it as a plain string. The result is remembered based on resource environment changes.
98 | *
99 | * Note: This function launches a coroutine to load the resources asynchronously.
100 | *
101 | * @return A plain string representation of the text content.
102 | */
103 | @Composable
104 | public override fun buildStringComposable(): String {
105 | val string by rememberResourceState({ "" }) {
106 | buildString()
107 | }
108 | return string
109 | }
110 |
111 | /**
112 | * Builds an annotated string representation of the text.
113 | *
114 | * This suspending function resolves the text content from its source (raw, resource, etc.)
115 | * and returns it as an [AnnotatedString] that preserves styling information.
116 | *
117 | * @return An [AnnotatedString] representation of the text content with styling.
118 | */
119 | public suspend fun buildAnnotatedString(): AnnotatedString {
120 | return when (val charSequence = build()) {
121 | is String -> AnnotatedString(charSequence)
122 | is AnnotatedString -> charSequence
123 | else -> AnnotatedString("")
124 | }
125 | }
126 |
127 | /**
128 | * Builds an annotated string representation of the text in a composable context.
129 | *
130 | * This composable function resolves the text content from its source and returns
131 | * it as an [AnnotatedString] with styling. The result is remembered based on
132 | * resource environment changes.
133 | *
134 | * Note: This function launches a coroutine to load the resources asynchronously.
135 | *
136 | * @return An [AnnotatedString] representation of the text content with styling.
137 | */
138 | @Composable
139 | public override fun buildAnnotatedStringComposable(): AnnotatedString {
140 | val annotatedString by rememberResourceState({ AnnotatedString("") }) {
141 | buildAnnotatedString()
142 | }
143 | return annotatedString
144 | }
145 |
146 | protected suspend fun resolveArg(arg: Any): Any = when (arg) {
147 | is UIText -> arg.build()
148 | else -> arg
149 | }
150 |
151 | protected fun hasAnnotations(
152 | args: List>>,
153 | resolvedArgs: List>>,
154 | baseAnnotations: List
155 | ): Boolean {
156 | if (baseAnnotations.isNotEmpty()) return true
157 | if (args.any { it.second.isNotEmpty() }) return true
158 | return resolvedArgs.any { it.first is AnnotatedString }
159 | }
160 |
161 | internal class Raw(private val text: CharSequence) : UIText() {
162 | override suspend fun build(): CharSequence {
163 | return text
164 | }
165 | }
166 |
167 | internal class Res(
168 | private val stringResource: StringResource,
169 | private val args: List>>,
170 | private val baseAnnotations: List
171 | ) : UIText() {
172 |
173 | override suspend fun build(): CharSequence {
174 | if (args.isEmpty() && baseAnnotations.isEmpty()) {
175 | return getString(stringResource)
176 | }
177 | val resolvedArgs = args.map {
178 | resolveArg(it.first) to it.second
179 | }
180 | val annotated = hasAnnotations(
181 | args = args,
182 | resolvedArgs = resolvedArgs,
183 | baseAnnotations = baseAnnotations
184 | )
185 | return if (annotated) {
186 | UITextUtil.buildAnnotatedStringWithComposeMultiplatformStringResourceRules(
187 | resolvedArgs = resolvedArgs,
188 | baseAnnotations = baseAnnotations,
189 | baseStringProvider = {
190 | getString(stringResource)
191 | }
192 | )
193 | } else if (resolvedArgs.isNotEmpty()) {
194 | val argValues = Array(resolvedArgs.size) { i ->
195 | resolvedArgs[i].first
196 | }
197 | getString(stringResource, *argValues)
198 | } else {
199 | getString(stringResource)
200 | }
201 | }
202 | }
203 |
204 | internal class PluralRes(
205 | private val pluralStringResource: PluralStringResource,
206 | private val quantity: Int,
207 | private val args: List>>,
208 | private val baseAnnotations: List
209 | ) : UIText() {
210 |
211 | override suspend fun build(): CharSequence {
212 | if (args.isEmpty() && baseAnnotations.isEmpty()) {
213 | return getPluralString(pluralStringResource, quantity)
214 | }
215 | val resolvedArgs = args.map {
216 | resolveArg(it.first) to it.second
217 | }
218 | val annotated = hasAnnotations(
219 | args = args,
220 | resolvedArgs = resolvedArgs,
221 | baseAnnotations = baseAnnotations
222 | )
223 | return if (annotated) {
224 | UITextUtil.buildAnnotatedStringWithComposeMultiplatformStringResourceRules(
225 | resolvedArgs = resolvedArgs,
226 | baseAnnotations = baseAnnotations,
227 | baseStringProvider = {
228 | getPluralString(pluralStringResource, quantity)
229 | }
230 | )
231 | } else if (resolvedArgs.isNotEmpty()) {
232 | val argValues = Array(resolvedArgs.size) { i ->
233 | resolvedArgs[i].first
234 | }
235 | getPluralString(pluralStringResource, quantity, *argValues)
236 | } else {
237 | getPluralString(pluralStringResource, quantity)
238 | }
239 | }
240 | }
241 |
242 | internal class Compound(
243 | private val components: List
244 | ) : UIText() {
245 | override suspend fun build(): CharSequence {
246 | if (components.isEmpty()) {
247 | return ""
248 | }
249 | if (components.size == 1) {
250 | return components[0].build()
251 | }
252 | val resolvedComponents = components.map { it.build() }
253 | return UITextUtil.concat(resolvedComponents)
254 | }
255 | }
256 | }
--------------------------------------------------------------------------------
/uitextcompose-multiplatform-sample/src/commonMain/kotlin/com/radusalagean/uitextcompose/multiplatform/sample/ui/screen/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.radusalagean.uitextcompose.multiplatform.sample.ui.screen
2 |
3 | import androidx.compose.runtime.derivedStateOf
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.setValue
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.text.LinkAnnotation
9 | import androidx.compose.ui.text.ParagraphStyle
10 | import androidx.compose.ui.text.SpanStyle
11 | import androidx.compose.ui.text.TextLinkStyles
12 | import androidx.compose.ui.text.font.FontWeight
13 | import androidx.compose.ui.text.style.TextDecoration
14 | import androidx.lifecycle.ViewModel
15 | import com.radusalagean.uitextcompose.multiplatform.UIText
16 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.component.ExampleEntryModel
17 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.component.LanguageOption
18 | import com.radusalagean.uitextcompose.multiplatform.sample.ui.theme.CustomGreen
19 | import com.radusalagean.uitextcompose.multiplatform.sample.util.LanguageManager
20 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.Res
21 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.greeting
22 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.language_english
23 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.language_romanian
24 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.legal_footer_example
25 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.legal_footer_example_insert_privacy_policy
26 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.legal_footer_example_insert_terms_of_service
27 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.proceed_to_checkout
28 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.products
29 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.section_title_examples
30 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.section_title_language
31 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.shopping_cart_status
32 | import ui_text_compose.uitextcompose_multiplatform_sample.generated.resources.shopping_cart_status_insert_shopping_cart
33 |
34 | class MainViewModel(
35 | private val languageManager: LanguageManager
36 | ) : ViewModel() {
37 |
38 | // Section: Language
39 | val languageSectionTitle = UIText { res(Res.string.section_title_language) }
40 | val languageOptions = listOf(
41 | LanguageOption(
42 | uiText = UIText { res(Res.string.language_english) },
43 | languageCode = "en"
44 | ),
45 | LanguageOption(
46 | uiText = UIText { res(Res.string.language_romanian) },
47 | languageCode = "ro"
48 | )
49 | )
50 | var selectedLanguageCode by mutableStateOf("")
51 | val selectedLanguageIndex: Int by derivedStateOf {
52 | languageOptions.indexOfFirst { it.languageCode == selectedLanguageCode }
53 | }
54 |
55 | fun syncSelectedLanguage() {
56 | selectedLanguageCode = languageManager.getCurrentLanguageCode()
57 | }
58 |
59 | fun onLanguageSelected(code: String) {
60 | languageManager.onLanguageSelected(code)
61 | }
62 |
63 | // Section: Examples
64 | val examplesSectionTitle = UIText { res(Res.string.section_title_examples) }
65 | val exampleEntries = listOf(
66 | ExampleEntryModel(
67 | label = "raw",
68 | value = UIText {
69 | raw("Radu")
70 | }
71 | ),
72 | ExampleEntryModel(
73 | label = "res",
74 | value = UIText {
75 | res(Res.string.greeting) {
76 | arg("Radu")
77 | }
78 | }
79 | ),
80 | ExampleEntryModel(
81 | label = "pluralRes",
82 | value = UIText {
83 | pluralRes(Res.plurals.products, 30)
84 | }
85 | ),
86 | ExampleEntryModel(
87 | label = "res - annotated",
88 | value = UIText {
89 | res(Res.string.shopping_cart_status) {
90 | arg(
91 | UIText {
92 | pluralRes(Res.plurals.products, 30)
93 | }
94 | )
95 | arg(
96 | UIText {
97 | res(Res.string.shopping_cart_status_insert_shopping_cart) {
98 | +SpanStyle(color = Color.Red)
99 | }
100 | }
101 | )
102 | }
103 | }
104 | ),
105 | ExampleEntryModel(
106 | label = "pluralRes - annotated",
107 | value = UIText {
108 | pluralRes(Res.plurals.products, 30) {
109 | arg(30.toString()) {
110 | +SpanStyle(color = CustomGreen)
111 | }
112 | +SpanStyle(fontWeight = FontWeight.Bold)
113 | }
114 | }
115 | ),
116 | ExampleEntryModel(
117 | label = "compound - example 1",
118 | value = UIText {
119 | res(Res.string.greeting) {
120 | arg("Radu")
121 | }
122 | raw(" ")
123 | res(Res.string.shopping_cart_status) {
124 | arg(
125 | UIText {
126 | pluralRes(Res.plurals.products, 30) {
127 | arg(30.toString()) {
128 | +SpanStyle(color = CustomGreen)
129 | }
130 | +SpanStyle(fontWeight = FontWeight.Bold)
131 | }
132 | }
133 | )
134 | arg(
135 | UIText {
136 | res(Res.string.shopping_cart_status_insert_shopping_cart) {
137 | +SpanStyle(color = Color.Red)
138 | }
139 | }
140 | )
141 | }
142 | }
143 | ),
144 | ExampleEntryModel(
145 | label = "compound - example 2",
146 | value = UIText {
147 | res(Res.string.greeting) {
148 | arg("Radu")
149 | }
150 | raw(" ")
151 | res(Res.string.shopping_cart_status) {
152 | arg(
153 | UIText {
154 | pluralRes(Res.plurals.products, 30) {
155 | arg(30.toString()) {
156 | +SpanStyle(color = CustomGreen)
157 | }
158 | +SpanStyle(fontWeight = FontWeight.Bold)
159 | }
160 | }
161 | )
162 | arg(
163 | UIText {
164 | res(Res.string.shopping_cart_status_insert_shopping_cart)
165 | }
166 | ) {
167 | +SpanStyle(color = Color.Red)
168 | }
169 | }
170 | raw(" ")
171 | res(Res.string.proceed_to_checkout) {
172 | +LinkAnnotation.Url(
173 | url = "https://example.com",
174 | styles = TextLinkStyles(
175 | style = SpanStyle(
176 | color = Color.Blue,
177 | textDecoration = TextDecoration.Underline
178 | )
179 | )
180 | )
181 | }
182 | }
183 | ),
184 | ExampleEntryModel(
185 | label = "compound - example 3",
186 | value = UIText {
187 | res(Res.string.greeting) {
188 | arg("Radu")
189 | }
190 | res(Res.string.shopping_cart_status) {
191 | +ParagraphStyle()
192 | arg(
193 | UIText {
194 | pluralRes(Res.plurals.products, 30) {
195 | +SpanStyle(fontWeight = FontWeight.Bold)
196 | arg(30.toString()) {
197 | +SpanStyle(color = CustomGreen)
198 | }
199 | }
200 | }
201 | )
202 | arg(
203 | UIText {
204 | res(Res.string.shopping_cart_status_insert_shopping_cart)
205 | }
206 | ) {
207 | +SpanStyle(color = Color.Red)
208 | }
209 | }
210 | res(Res.string.proceed_to_checkout) {
211 | +LinkAnnotation.Url(
212 | url = "https://example.com",
213 | styles = TextLinkStyles(
214 | style = SpanStyle(
215 | color = Color.Blue,
216 | textDecoration = TextDecoration.Underline
217 | )
218 | )
219 | )
220 | }
221 | }
222 | ),
223 | ExampleEntryModel(
224 | label = "terms of service & privacy policy",
225 | value = UIText {
226 | val linkStyle = TextLinkStyles(
227 | SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)
228 | )
229 | res(Res.string.legal_footer_example) {
230 | arg(
231 | UIText {
232 | res(Res.string.legal_footer_example_insert_terms_of_service)
233 | }
234 | ) {
235 | +LinkAnnotation.Url(
236 | url = "https://radusalagean.com/example-terms-of-service/",
237 | styles = linkStyle
238 | )
239 | }
240 | arg(
241 | UIText {
242 | res(Res.string.legal_footer_example_insert_privacy_policy)
243 | }
244 | ) {
245 | +LinkAnnotation.Url(
246 | url = "https://radusalagean.com/example-privacy-policy/",
247 | styles = linkStyle
248 | )
249 | }
250 | }
251 | }
252 | )
253 | )
254 | }
--------------------------------------------------------------------------------