Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(explore): start to integrate search UI flow #3

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ android {
versionName = "${libs.versions.app.version.name.get()} (${SimpleDateFormat("MMddyyyy").format(Date())})"
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
Expand Down Expand Up @@ -53,4 +54,5 @@ dependencies {
implementation(project(":shared"))
implementation(libs.kotlin.inject.runtime)
ksp(libs.kotlin.inject.compiler)
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
}
12 changes: 7 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ app-version-code = "1"
app-version-name = "1.0.0"
android-compileSdk = "34"
android-minSdk = "24"
agp = "8.3.0-alpha11"
kotlin = "1.9.10"
agp = "8.3.0-alpha14"
kotlin = "1.9.20"
compose-plugin = "1.5.10"
compose-compiler = "1.5.3"
compose-compiler = "1.5.4"
mapsCompose = "4.1.1"
voyager = "1.0.0-rc07"
voyager = "1.0.0-rc10"
kotlinInject = "0.6.1"
settings = "1.1.0"

Expand All @@ -29,12 +29,14 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa

kotlin-inject-compiler = { module = 'me.tatarka.inject:kotlin-inject-compiler-ksp', version.ref = 'kotlinInject' }
kotlin-inject-runtime = { module = 'me.tatarka.inject:kotlin-inject-runtime', version.ref = 'kotlinInject' }
kotlin-viewmodel-core = { module = "com.rickclephas.kmm:kmm-viewmodel-core", version = "1.0.0-ALPHA-14"}
kotlin-viewmodel-core = { module = "com.rickclephas.kmm:kmm-viewmodel-core", version = "1.0.0-ALPHA-14" }

imageloader = { module = "io.github.qdsfdhvh:image-loader", version = "1.6.8" }

logging = { module = "org.lighthousegames:logging", version = "1.3.0" }

orbital = { module = "com.github.skydoves:orbital", version = "0.3.2" }

settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "settings" }
settings-coroutines = { module = "com.russhwolf:multiplatform-settings-coroutines", version.ref = "settings" }
settings-datastore = { module = "com.russhwolf:multiplatform-settings-datastore", version.ref = "settings" }
Expand Down
6 changes: 6 additions & 0 deletions shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ kotlin {
// Must define the pods that are in the Podfile (unknown why?)
pod("GoogleMaps") {
version = "8.2.0"
extraOpts += listOf("-compiler-option", "-fmodules")
}
}

Expand All @@ -61,6 +62,7 @@ kotlin {
implementation(libs.kotlinx.datetime)
implementation(libs.imageloader)
implementation(libs.logging)
implementation(libs.orbital)
implementation(libs.settings)
implementation(libs.settings.coroutines)
implementation(libs.webview)
Expand Down Expand Up @@ -95,7 +97,10 @@ android {
minSdk = libs.versions.android.minSdk.get().toInt()
}


compileOptions {
// Enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
Expand Down Expand Up @@ -126,4 +131,5 @@ dependencies {
add("kspIosArm64", libs.kotlin.inject.compiler)
add("kspIosSimulatorArm64", libs.kotlin.inject.compiler)
add("kspAndroid", libs.kotlin.inject.compiler)
add("coreLibraryDesugaring", "com.android.tools:desugar_jdk_libs:2.0.4")
}
11 changes: 11 additions & 0 deletions shared/shared.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ Pod::Spec.new do |spec|
spec.ios.deployment_target = '14.1'
spec.dependency 'GoogleMaps', '8.2.0'

if !Dir.exist?('build/cocoapods/framework/shared.framework') || Dir.empty?('build/cocoapods/framework/shared.framework')
raise "

Kotlin framework 'shared' doesn't exist yet, so a proper Xcode project can't be generated.
'pod install' should be executed after running ':generateDummyFramework' Gradle task:

./gradlew :shared:generateDummyFramework

Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
end

spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':shared',
'PRODUCT_MODULE_NAME' => 'shared',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.airbnb.sample.ui.components

import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight
import com.airbnb.sample.theme.dimens
import com.airbnb.sample.utils.ui.NoRippleInteractionSource


@Composable
actual fun <T> SegmentedControl(
modifier: Modifier,
options: List<T>,
selected: T,
onOptionClicked: (T) -> Unit,
titleForItem: (T) -> String,
) {
val contentMargin = MaterialTheme.dimens.staticGrid.x2
val offset by animateFloatAsState(
when (options.indexOf(selected)) {
0 -> 0f
1 -> 1f
2 -> 2f
else -> 0f
}
)
androidx.compose.foundation.layout.Row(
modifier = modifier
.height(IntrinsicSize.Min)
.background(
Color(0xFFEEEEEF),
CircleShape,
)
.drawWithContent {
drawRoundRect(
color = Color.White,
topLeft = Offset(
contentMargin.value + (size.width / 3f * offset),
contentMargin.value
),
size = Size(
this.size.width / 3f - contentMargin.value * 2,
this.size.height - contentMargin.value * 2
),
cornerRadius = CornerRadius(this.size.height / 2)
)
drawContent()
}
.clip(CircleShape),
verticalAlignment = Alignment.CenterVertically,
) {
options.onEachIndexed { index, segment ->
Box(
Modifier
.fillMaxHeight()
.clip(CircleShape)
.clickable(
enabled = true,
onClickLabel = titleForItem(segment),
indication = null,
role = Role.Button,
interactionSource = NoRippleInteractionSource()
) { onOptionClicked(segment) }
.padding(contentMargin)
// Divide space evenly between all segments.
.weight(1f),
contentAlignment = Alignment.Center,
) {
Text(
text = titleForItem(segment),
style = MaterialTheme.typography.bodySmall.copy(fontWeight = FontWeight.W600)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,37 @@
package com.airbnb.sample.viewmodel

import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras as AndroidXCreationExtras
import androidx.lifecycle.viewmodel.CreationExtras.Key as AndroidXCreationExtrasKey
import androidx.lifecycle.viewmodel.MutableCreationExtras as AndroidXMutableCreationExtras

actual typealias CreationExtras = AndroidXCreationExtras

actual typealias CreationExtrasKey<T> = AndroidXCreationExtrasKey<T>

actual typealias EmptyCreationExtras = AndroidXCreationExtras.Empty
actual abstract class CreationExtras internal actual constructor() {
internal val map: MutableMap<Key<*>, Any?> = mutableMapOf()

actual abstract operator fun <T> get(key: Key<T>): T?
}

actual object EmptyCreationExtras : CreationExtras() {
override fun <T> get(key: Key<T>): T? = null
}

actual class MutableCreationExtras actual constructor(initialExtras: CreationExtras) : CreationExtras() {
init {
map.putAll(initialExtras.map)
}

/**
* Associates the given [key] with [t]
*/
actual operator fun <T> set(key: Key<T>, t: T) {
map[key] = t
}

actual typealias MutableCreationExtras = AndroidXMutableCreationExtras
override fun <T> get(key: Key<T>): T? {
@Suppress("UNCHECKED_CAST")
return map[key] as T?
}
}

/**
* @see [ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.kizitonwose.calendar.core

import kotlinx.datetime.DayOfWeek
import org.threeten.bp.temporal.WeekFields
import java.util.Locale

actual fun firstDayOfWeekFromLocale(): DayOfWeek {
return DayOfWeek(WeekFields.of(Locale.getDefault()).firstDayOfWeek.value)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.kizitonwose.calendar.data

import kotlinx.datetime.DayOfWeek


actual fun DayOfWeek.daysUntil(other: DayOfWeek): Int {
val symbols = org.threeten.bp.DayOfWeek.entries.map { it.name.uppercase() }
val start = symbols.indexOf(this.name)
val end = symbols.indexOf(other.name)

return if (end < start) {
7 - start + end
} else {
end - start
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.airbnb.sample.data.explore

import com.airbnb.sample.utils.printed
import com.kizitonwose.calendar.core.DayOfWeekProgression
import com.kizitonwose.calendar.core.YearMonth
import com.kizitonwose.calendar.core.nextMonth
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Duration.Companion.days

enum class SearchDateParameterType {
Dates, Months, Flexible
}
sealed interface SearchDateParameters {
val type: SearchDateParameterType

data class Dates(val selectedDates: List<Instant>, val offset: DateWindowOffset): SearchDateParameters {
val selectedRange: ClosedRange<Instant>?
get() {
if (selectedDates.isEmpty()) return null
return selectedDates.first().rangeTo(selectedDates.last())
}

override val type: SearchDateParameterType = SearchDateParameterType.Dates
}
data class Months(val duration: Int): SearchDateParameters {
override val type: SearchDateParameterType = SearchDateParameterType.Months
}
data class Flexible(val criteria: FlexibleDateCriteria): SearchDateParameters {
override val type: SearchDateParameterType = SearchDateParameterType.Flexible
}

fun hasDateSelections() = when (this) {
is Dates -> selectedDates.isNotEmpty() || offset != DateWindowOffset.None
is Flexible -> criteria.length != FlexibleDateCriteria.StayLength.Weekend || criteria.months.isNotEmpty()
is Months -> duration != 3
}

fun printed(): String {
return when (this) {
is Dates -> {
return when (offset) {
is DateWindowOffset.Count -> {
val moveBy = offset.count
val adjustedStart = selectedRange?.start?.minus(moveBy.days)
selectedRange?.endInclusive?.plus(moveBy.days)?.let {
adjustedStart?.rangeTo(it)?.printed().orEmpty()
}.orEmpty()

}
DateWindowOffset.None -> selectedRange?.printed().orEmpty()
}
}
is Flexible -> {
val dates = criteria.months
if (dates.count() == 12) {
// anytime
return "Any ${criteria.length::class.simpleName?.lowercase()}"
}

val months = dates.map { it.monthName() }

when (criteria.length) {
FlexibleDateCriteria.StayLength.Month -> {
if (months.count() > 1) {
return "Month in ${months.joinToString(", ") { it.take(3) }}"
}
"Month in ${months.first()}"
}
FlexibleDateCriteria.StayLength.Week -> {
if (months.count() > 1) {
return "Week in ${months.joinToString(", ") { it.take(3) }}"
}
"Week in ${months.first()}"
}
FlexibleDateCriteria.StayLength.Weekend -> {
if (months.count() > 1) {
return "Weekend in ${months.joinToString(", ") { it.take(3) }}"
}
"Weekend in ${months.first()}"
}
}
}
is Months -> {
val currentMonth = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
.let { YearMonth(month = it.monthNumber, year = it.year) }

val nextMonth = currentMonth.nextMonth
val endMonth = nextMonth.plusMonths(duration)

return "${nextMonth.printed(withDay = 1)} - ${endMonth.printed(withDay = 1)}"
}
}
}
}
Loading