diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index e82b904..75cf723 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -12,10 +12,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -25,4 +25,4 @@ jobs: uses: android-actions/setup-android@v3 - name: Build with Gradle - run: ./gradlew composeApp:buildDebug --no-daemon + run: ./gradlew composeApp:build --no-daemon diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 042d3ad..7e7be7b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -11,10 +11,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' @@ -24,13 +24,12 @@ jobs: uses: android-actions/setup-android@v3 - name: Build with Gradle - run: ./gradlew composeApp:buildDebug --no-daemon + run: ./gradlew composeApp:build --no-daemon - name: Gradle Release run: ./gradlew composeApp:debug --no-daemon - - name: Upload artifacts - uses: actions/upload-artifact@v4 + - name: Release + uses: softprops/action-gh-release@v2 with: - name: app - path: composeApp/build/outputs/apk/debug/*.apk + files: composeApp/build/outputs/apk/debug/*.apk diff --git a/README.md b/README.md index 1ba257b..6a09ff1 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,39 @@ -This is a Kotlin Multiplatform project targeting Android, iOS. +# Bob -- The Handy Feedback App -* `/composeApp` is for code that will be shared across your Compose Multiplatform applications. - It contains several subfolders: - - `commonMain` is for code that’s common for all targets. - - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. - For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, - `iosMain` would be the right folder for such calls. +This project is a simple Kotlin Multiplatform Application that allows users to enter free-text and +submit the content to a server over HTTP. It also includes a form with sentiment selection and +displays a Snackbar whenever the submit button is pressed. -* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, - you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. +## Features +- Free-text input form +- Sentiment selection using FilterChips +- Submit content to a server using Retrofit +- Display Snackbar on form submission -Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… \ No newline at end of file +## Technologies Used + +- Kotlin +- Jetpack Compose +- Retrofit +- Gradle + +## Getting Started + +### Installation + +1. Download the latest release from + the [releases page](https://git.brmartin.co.uk/bob/mobile-application/releases) +2. Install the application following the on-screen instructions. + +### Usage + +1. Run the application on an Android emulator or a physical device. +2. Select a sentiment using the FilterChips. +3. Enter your text in the provided text field. +4. Press the submit button to send the content to the server. + +### Project Structure + +- `composeApp/src/commonMain/kotlin/uk/sky/bob/application/App.kt`: Main Compose UI and form + submission logic. diff --git a/composeApp/src/commonMain/kotlin/uk/sky/bob/application/App.kt b/composeApp/src/commonMain/kotlin/uk/sky/bob/application/App.kt index ca19bda..4793c10 100644 --- a/composeApp/src/commonMain/kotlin/uk/sky/bob/application/App.kt +++ b/composeApp/src/commonMain/kotlin/uk/sky/bob/application/App.kt @@ -1,37 +1,102 @@ package uk.sky.bob.application -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.material.Button +import androidx.compose.material.ChipDefaults +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.FilterChip +import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold import androidx.compose.material.Text -import androidx.compose.runtime.* +import androidx.compose.material.TextField +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import org.jetbrains.compose.resources.painterResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.jetbrains.compose.ui.tooling.preview.Preview -import bob.composeapp.generated.resources.Res -import bob.composeapp.generated.resources.compose_multiplatform - +@OptIn(ExperimentalMaterialApi::class) @Composable @Preview fun App() { MaterialTheme { - var showContent by remember { mutableStateOf(false) } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Button(onClick = { showContent = !showContent }) { - Text("Click me!") - } - AnimatedVisibility(showContent) { - val greeting = remember { Greeting().greet() } - Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { - Image(painterResource(Res.drawable.compose_multiplatform), null) - Text("Compose: $greeting") + val scaffoldState = rememberScaffoldState() + val scope = rememberCoroutineScope() + + Scaffold(scaffoldState = scaffoldState) { + Box( + modifier = Modifier.fillMaxHeight(), + contentAlignment = Alignment.Center, + ) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val sentiment = remember { mutableStateOf(Sentiment.HAPPY) } + var comment by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf( + TextFieldValue("") + ) + } + Row(modifier = Modifier.padding(8.dp)) { + for (emotion in Sentiment.entries) { + FilterChip( + onClick = { sentiment.value = emotion }, + selected = sentiment.value == emotion, + modifier = Modifier.padding(8.dp), + leadingIcon = { Text(emotion.leadingIcon, fontSize = 20.sp) }, + colors = ChipDefaults.outlinedFilterChipColors(), + border = ChipDefaults.outlinedBorder, + ) { + Text(emotion.friendlyName, fontSize = 20.sp) + } + } + } + + Row(modifier = Modifier.padding(8.dp)) { + TextField( + value = comment, + onValueChange = { comment = it }, + label = { Text("Your comment") }, + modifier = Modifier.height(100.dp).fillMaxWidth().padding(8.dp), + ) + } + + Row(modifier = Modifier.padding(8.dp)) { + Button(onClick = { + sentiment.value = Sentiment.HAPPY + comment = TextFieldValue("") + scope.launch { + delay(1000) + scaffoldState.snackbarHostState.showSnackbar("Feedback sent") + } + }, modifier = Modifier.padding(8.dp)) { + Icon(Icons.AutoMirrored.Filled.Send, contentDescription = "Send") + } + } } } } } -} \ No newline at end of file +} + diff --git a/composeApp/src/commonMain/kotlin/uk/sky/bob/application/Greeting.kt b/composeApp/src/commonMain/kotlin/uk/sky/bob/application/Greeting.kt deleted file mode 100644 index 83a450f..0000000 --- a/composeApp/src/commonMain/kotlin/uk/sky/bob/application/Greeting.kt +++ /dev/null @@ -1,9 +0,0 @@ -package uk.sky.bob.application - -class Greeting { - private val platform = getPlatform() - - fun greet(): String { - return "Hello, ${platform.name}!" - } -} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/uk/sky/bob/application/Sentiment.kt b/composeApp/src/commonMain/kotlin/uk/sky/bob/application/Sentiment.kt new file mode 100644 index 0000000..bfe83d2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/uk/sky/bob/application/Sentiment.kt @@ -0,0 +1,7 @@ +package uk.sky.bob.application + +enum class Sentiment(val friendlyName: String, val leadingIcon: String) { + HAPPY("Happy", "\uD83D\uDE03"), + NEUTRAL("Neutral", "\uD83D\uDE10"), + SAD("Sad", "\uD83D\uDE1E"); +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d9ed41f..1e888c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,19 @@ [versions] -agp = "8.5.2" -android-compileSdk = "34" +agp = "8.10.1" +android-compileSdk = "35" android-minSdk = "24" android-targetSdk = "34" -androidx-activityCompose = "1.9.3" -androidx-appcompat = "1.7.0" -androidx-constraintlayout = "2.2.0" -androidx-core-ktx = "1.15.0" +androidx-activityCompose = "1.10.1" +androidx-appcompat = "1.7.1" +androidx-constraintlayout = "2.2.1" +androidx-core-ktx = "1.16.0" androidx-espresso-core = "3.6.1" -androidx-lifecycle = "2.8.4" +androidx-lifecycle = "2.9.1" androidx-material = "1.12.0" androidx-test-junit = "1.2.1" -compose-multiplatform = "1.7.0" +compose-multiplatform = "1.7.3" junit = "4.13.2" -kotlin = "2.1.0" +kotlin = "2.1.21" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 09523c0..ff23a68 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..7190a60 --- /dev/null +++ b/renovate.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json" +}