mirror of
https://github.com/jlengrand/compose-multiplatform.git
synced 2026-03-10 08:11:20 +00:00
Add web app for the TodoApp example (#778)
* Prepare the TodoApp example for adding the JavaScript app * Add the JavaScript app for the TodoApp example * TodoApp. Update Compose to 0.5.0-build225.
This commit is contained in:
@@ -10,11 +10,11 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen
|
||||
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
|
||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
||||
import com.arkivanov.mvikotlin.timetravel.store.TimeTravelStoreFactory
|
||||
import example.todo.common.database.DefaultTodoSharedDatabase
|
||||
import example.todo.common.database.TodoDatabaseDriver
|
||||
import example.todo.common.root.TodoRoot
|
||||
import example.todo.common.root.integration.TodoRootComponent
|
||||
import example.todo.common.ui.TodoRootContent
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -33,6 +33,6 @@ class MainActivity : AppCompatActivity() {
|
||||
TodoRootComponent(
|
||||
componentContext = componentContext,
|
||||
storeFactory = LoggingStoreFactory(TimeTravelStoreFactory(DefaultStoreFactory)),
|
||||
database = TodoDatabase(TodoDatabaseDriver(context = this))
|
||||
database = DefaultTodoSharedDatabase(TodoDatabaseDriver(context = this))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,13 @@ object Deps {
|
||||
const val gradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:$VERSION"
|
||||
const val testCommon = "org.jetbrains.kotlin:kotlin-test-common:$VERSION"
|
||||
const val testJunit = "org.jetbrains.kotlin:kotlin-test-junit:$VERSION"
|
||||
const val testJs = "org.jetbrains.kotlin:kotlin-test-js:$VERSION"
|
||||
const val testAnnotationsCommon = "org.jetbrains.kotlin:kotlin-test-annotations-common:$VERSION"
|
||||
}
|
||||
|
||||
object Compose {
|
||||
// __LATEST_COMPOSE_RELEASE_VERSION__
|
||||
private const val VERSION = "0.4.0"
|
||||
private const val VERSION = "0.5.0-build225"
|
||||
const val gradlePlugin = "org.jetbrains.compose:compose-gradle-plugin:$VERSION"
|
||||
}
|
||||
}
|
||||
@@ -37,7 +38,7 @@ object Deps {
|
||||
|
||||
object ArkIvanov {
|
||||
object MVIKotlin {
|
||||
private const val VERSION = "2.0.2"
|
||||
private const val VERSION = "2.0.3"
|
||||
const val rx = "com.arkivanov.mvikotlin:rx:$VERSION"
|
||||
const val mvikotlin = "com.arkivanov.mvikotlin:mvikotlin:$VERSION"
|
||||
const val mvikotlinMain = "com.arkivanov.mvikotlin:mvikotlin-main:$VERSION"
|
||||
@@ -71,6 +72,7 @@ object Deps {
|
||||
const val androidDriver = "com.squareup.sqldelight:android-driver:$VERSION"
|
||||
const val sqliteDriver = "com.squareup.sqldelight:sqlite-driver:$VERSION"
|
||||
const val nativeDriver = "com.squareup.sqldelight:native-driver:$VERSION"
|
||||
const val sqljsDriver = "com.squareup.sqldelight:sqljs-driver:$VERSION"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ kotlin {
|
||||
android()
|
||||
ios()
|
||||
|
||||
js(IR) {
|
||||
browser()
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
named("commonTest") {
|
||||
dependencies {
|
||||
@@ -26,6 +30,11 @@ kotlin {
|
||||
implementation(Deps.JetBrains.Kotlin.testJunit)
|
||||
}
|
||||
}
|
||||
named("jsTest") {
|
||||
dependencies {
|
||||
implementation(Deps.JetBrains.Kotlin.testJs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
|
||||
|
||||
@@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
@@ -130,6 +131,7 @@ private fun Item(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun TodoInput(
|
||||
text: String,
|
||||
|
||||
@@ -36,5 +36,11 @@ kotlin {
|
||||
implementation(Deps.Squareup.SQLDelight.nativeDriver)
|
||||
}
|
||||
}
|
||||
|
||||
jsMain {
|
||||
dependencies {
|
||||
implementation(Deps.Squareup.SQLDelight.sqljsDriver)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
@Suppress("FunctionName") // FactoryFunction
|
||||
actual fun TestDatabaseDriver(): SqlDriver {
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
TodoDatabase.Schema.create(driver)
|
||||
|
||||
return driver
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.badoo.reaktive.base.setCancellable
|
||||
import com.badoo.reaktive.completable.Completable
|
||||
import com.badoo.reaktive.maybe.Maybe
|
||||
import com.badoo.reaktive.observable.Observable
|
||||
import com.badoo.reaktive.observable.autoConnect
|
||||
import com.badoo.reaktive.observable.firstOrError
|
||||
import com.badoo.reaktive.observable.map
|
||||
import com.badoo.reaktive.observable.observable
|
||||
import com.badoo.reaktive.observable.observeOn
|
||||
import com.badoo.reaktive.observable.replay
|
||||
import com.badoo.reaktive.scheduler.ioScheduler
|
||||
import com.badoo.reaktive.single.Single
|
||||
import com.badoo.reaktive.single.asCompletable
|
||||
import com.badoo.reaktive.single.asObservable
|
||||
import com.badoo.reaktive.single.doOnBeforeSuccess
|
||||
import com.badoo.reaktive.single.flatMapObservable
|
||||
import com.badoo.reaktive.single.map
|
||||
import com.badoo.reaktive.single.mapNotNull
|
||||
import com.badoo.reaktive.single.observeOn
|
||||
import com.badoo.reaktive.single.singleOf
|
||||
import com.squareup.sqldelight.Query
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
class DefaultTodoSharedDatabase(driver: Single<SqlDriver>) : TodoSharedDatabase {
|
||||
|
||||
constructor(driver: SqlDriver) : this(singleOf(driver))
|
||||
|
||||
private val queries: Single<TodoDatabaseQueries> =
|
||||
driver
|
||||
.map { TodoDatabase(it).todoDatabaseQueries }
|
||||
.asObservable()
|
||||
.replay()
|
||||
.autoConnect()
|
||||
.firstOrError()
|
||||
|
||||
override fun observeAll(): Observable<List<TodoItemEntity>> =
|
||||
query(TodoDatabaseQueries::selectAll)
|
||||
.observe { it.executeAsList() }
|
||||
|
||||
override fun select(id: Long): Maybe<TodoItemEntity> =
|
||||
query { it.select(id = id) }
|
||||
.mapNotNull { it.executeAsOneOrNull() }
|
||||
|
||||
override fun add(text: String): Completable =
|
||||
execute { it.add(text = text) }
|
||||
|
||||
override fun setText(id: Long, text: String): Completable =
|
||||
execute { it.setText(id = id, text = text) }
|
||||
|
||||
override fun setDone(id: Long, isDone: Boolean): Completable =
|
||||
execute { it.setDone(id = id, isDone = isDone) }
|
||||
|
||||
override fun delete(id: Long): Completable =
|
||||
execute { it.delete(id = id) }
|
||||
|
||||
override fun clear(): Completable =
|
||||
execute { it.clear() }
|
||||
|
||||
private fun <T : Any> query(query: (TodoDatabaseQueries) -> Query<T>): Single<Query<T>> =
|
||||
queries
|
||||
.observeOn(ioScheduler)
|
||||
.map(query)
|
||||
|
||||
private fun execute(query: (TodoDatabaseQueries) -> Unit): Completable =
|
||||
queries
|
||||
.observeOn(ioScheduler)
|
||||
.doOnBeforeSuccess(query)
|
||||
.asCompletable()
|
||||
|
||||
private fun <T : Any, R> Single<Query<T>>.observe(get: (Query<T>) -> R): Observable<R> =
|
||||
flatMapObservable { it.observed() }
|
||||
.observeOn(ioScheduler)
|
||||
.map(get)
|
||||
|
||||
private fun <T : Any> Query<T>.observed(): Observable<Query<T>> =
|
||||
observable { emitter ->
|
||||
val listener =
|
||||
object : Query.Listener {
|
||||
override fun queryResultsChanged() {
|
||||
emitter.onNext(this@observed)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onNext(this@observed)
|
||||
addListener(listener)
|
||||
emitter.setCancellable { removeListener(listener) }
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.badoo.reaktive.base.setCancellable
|
||||
import com.badoo.reaktive.observable.Observable
|
||||
import com.badoo.reaktive.observable.map
|
||||
import com.badoo.reaktive.observable.observable
|
||||
import com.badoo.reaktive.observable.observeOn
|
||||
import com.badoo.reaktive.scheduler.ioScheduler
|
||||
import com.squareup.sqldelight.Query
|
||||
|
||||
fun <T : Any, R> Query<T>.asObservable(execute: (Query<T>) -> R): Observable<R> =
|
||||
asObservable()
|
||||
.observeOn(ioScheduler)
|
||||
.map(execute)
|
||||
|
||||
fun <T : Any> Query<T>.asObservable(): Observable<Query<T>> =
|
||||
observable { emitter ->
|
||||
val listener =
|
||||
object : Query.Listener {
|
||||
override fun queryResultsChanged() {
|
||||
emitter.onNext(this@asObservable)
|
||||
}
|
||||
}
|
||||
|
||||
emitter.onNext(this@asObservable)
|
||||
addListener(listener)
|
||||
emitter.setCancellable { removeListener(listener) }
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
|
||||
@Suppress("FunctionName")
|
||||
expect fun TestDatabaseDriver(): SqlDriver
|
||||
@@ -0,0 +1,105 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.badoo.reaktive.base.invoke
|
||||
import com.badoo.reaktive.completable.Completable
|
||||
import com.badoo.reaktive.completable.completableFromFunction
|
||||
import com.badoo.reaktive.completable.observeOn
|
||||
import com.badoo.reaktive.maybe.Maybe
|
||||
import com.badoo.reaktive.maybe.observeOn
|
||||
import com.badoo.reaktive.observable.Observable
|
||||
import com.badoo.reaktive.observable.map
|
||||
import com.badoo.reaktive.observable.observeOn
|
||||
import com.badoo.reaktive.scheduler.Scheduler
|
||||
import com.badoo.reaktive.single.notNull
|
||||
import com.badoo.reaktive.single.singleFromFunction
|
||||
import com.badoo.reaktive.subject.behavior.BehaviorSubject
|
||||
|
||||
// There were problems when using real database in JS tests, hence the in-memory test implementation
|
||||
class TestTodoSharedDatabase(
|
||||
private val scheduler: Scheduler
|
||||
) : TodoSharedDatabase {
|
||||
|
||||
private val itemsSubject = BehaviorSubject<Map<Long, TodoItemEntity>>(emptyMap())
|
||||
private val itemsObservable = itemsSubject.observeOn(scheduler)
|
||||
val testing: Testing = Testing()
|
||||
|
||||
override fun observeAll(): Observable<List<TodoItemEntity>> =
|
||||
itemsObservable.map { it.values.toList() }
|
||||
|
||||
override fun select(id: Long): Maybe<TodoItemEntity> =
|
||||
singleFromFunction { testing.select(id = id) }
|
||||
.notNull()
|
||||
.observeOn(scheduler)
|
||||
|
||||
override fun add(text: String): Completable =
|
||||
execute { testing.add(text = text) }
|
||||
|
||||
override fun setText(id: Long, text: String): Completable =
|
||||
execute { testing.setText(id = id, text = text) }
|
||||
|
||||
override fun setDone(id: Long, isDone: Boolean): Completable =
|
||||
execute { testing.setDone(id = id, isDone = isDone) }
|
||||
|
||||
override fun delete(id: Long): Completable =
|
||||
execute { testing.delete(id = id) }
|
||||
|
||||
override fun clear(): Completable =
|
||||
execute { testing.clear() }
|
||||
|
||||
private fun execute(block: () -> Unit): Completable =
|
||||
completableFromFunction(block)
|
||||
.observeOn(scheduler)
|
||||
|
||||
inner class Testing {
|
||||
fun select(id: Long): TodoItemEntity? =
|
||||
itemsSubject.value[id]
|
||||
|
||||
fun selectRequired(id: Long): TodoItemEntity =
|
||||
requireNotNull(select(id = id))
|
||||
|
||||
fun add(text: String) {
|
||||
updateItems { items ->
|
||||
val nextId = items.keys.maxOrNull()?.plus(1L) ?: 1L
|
||||
|
||||
val item =
|
||||
TodoItemEntity(
|
||||
id = nextId,
|
||||
orderNum = items.size.toLong(),
|
||||
text = text,
|
||||
isDone = false
|
||||
)
|
||||
|
||||
items + (nextId to item)
|
||||
}
|
||||
}
|
||||
|
||||
fun setText(id: Long, text: String) {
|
||||
updateItem(id = id) { it.copy(text = text) }
|
||||
}
|
||||
|
||||
fun setDone(id: Long, isDone: Boolean) {
|
||||
updateItem(id = id) { it.copy(isDone = isDone) }
|
||||
}
|
||||
|
||||
fun delete(id: Long) {
|
||||
updateItems { it - id }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
updateItems { emptyMap() }
|
||||
}
|
||||
|
||||
fun getLastInsertId(): Long? =
|
||||
itemsSubject.value.values.lastOrNull()?.id
|
||||
|
||||
private fun updateItems(func: (Map<Long, TodoItemEntity>) -> Map<Long, TodoItemEntity>) {
|
||||
itemsSubject(func(itemsSubject.value))
|
||||
}
|
||||
|
||||
private fun updateItem(id: Long, func: (TodoItemEntity) -> TodoItemEntity) {
|
||||
updateItems {
|
||||
it + (id to it.getValue(id).let(func))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.badoo.reaktive.completable.Completable
|
||||
import com.badoo.reaktive.maybe.Maybe
|
||||
import com.badoo.reaktive.observable.Observable
|
||||
|
||||
interface TodoSharedDatabase {
|
||||
|
||||
fun observeAll(): Observable<List<TodoItemEntity>>
|
||||
|
||||
fun select(id: Long): Maybe<TodoItemEntity>
|
||||
|
||||
fun add(text: String): Completable
|
||||
|
||||
fun setText(id: Long, text: String): Completable
|
||||
|
||||
fun setDone(id: Long, isDone: Boolean): Completable
|
||||
|
||||
fun delete(id: Long): Completable
|
||||
|
||||
fun clear(): Completable
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.sqlite.driver.JdbcSqliteDriver
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
@Suppress("FunctionName") // FactoryFunction
|
||||
actual fun TestDatabaseDriver(): SqlDriver {
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
TodoDatabase.Schema.create(driver)
|
||||
|
||||
return driver
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import co.touchlab.sqliter.DatabaseConfiguration
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.drivers.native.NativeSqliteDriver
|
||||
import com.squareup.sqldelight.drivers.native.wrapConnection
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
@Suppress("FunctionName") // Factory function
|
||||
actual fun TestDatabaseDriver(): SqlDriver {
|
||||
val schema = TodoDatabase.Schema
|
||||
|
||||
return NativeSqliteDriver(
|
||||
DatabaseConfiguration(
|
||||
name = ":memory:",
|
||||
version = schema.version,
|
||||
create = { wrapConnection(it, schema::create) },
|
||||
upgrade = { connection, oldVersion, newVersion ->
|
||||
wrapConnection(connection) {
|
||||
schema.migrate(it, oldVersion, newVersion)
|
||||
}
|
||||
},
|
||||
inMemory = true
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package example.todo.common.database
|
||||
|
||||
import com.badoo.reaktive.promise.asSingle
|
||||
import com.badoo.reaktive.single.Single
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.drivers.sqljs.initSqlDriver
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
fun todoDatabaseDriver(): Single<SqlDriver> =
|
||||
initSqlDriver(TodoDatabase.Schema).asSingle()
|
||||
@@ -6,6 +6,7 @@ import com.arkivanov.decompose.value.operator.map
|
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory
|
||||
import com.badoo.reaktive.base.Consumer
|
||||
import com.badoo.reaktive.base.invoke
|
||||
import example.todo.common.database.TodoSharedDatabase
|
||||
import example.todo.common.edit.TodoEdit
|
||||
import example.todo.common.edit.TodoEdit.Model
|
||||
import example.todo.common.edit.TodoEdit.Output
|
||||
@@ -13,12 +14,11 @@ import example.todo.common.edit.store.TodoEditStore.Intent
|
||||
import example.todo.common.edit.store.TodoEditStoreProvider
|
||||
import example.todo.common.utils.asValue
|
||||
import example.todo.common.utils.getStore
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
class TodoEditComponent(
|
||||
componentContext: ComponentContext,
|
||||
storeFactory: StoreFactory,
|
||||
database: TodoDatabase,
|
||||
database: TodoSharedDatabase,
|
||||
itemId: Long,
|
||||
private val output: Consumer<Output>
|
||||
) : TodoEdit, ComponentContext by componentContext {
|
||||
@@ -27,7 +27,7 @@ class TodoEditComponent(
|
||||
instanceKeeper.getStore {
|
||||
TodoEditStoreProvider(
|
||||
storeFactory = storeFactory,
|
||||
database = TodoEditStoreDatabase(queries = database.todoDatabaseQueries),
|
||||
database = TodoEditStoreDatabase(database = database),
|
||||
id = itemId
|
||||
).provide()
|
||||
}
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
package example.todo.common.edit.integration
|
||||
|
||||
import com.badoo.reaktive.completable.Completable
|
||||
import com.badoo.reaktive.completable.completableFromFunction
|
||||
import com.badoo.reaktive.completable.subscribeOn
|
||||
import com.badoo.reaktive.maybe.Maybe
|
||||
import com.badoo.reaktive.maybe.map
|
||||
import com.badoo.reaktive.maybe.maybeFromFunction
|
||||
import com.badoo.reaktive.maybe.notNull
|
||||
import com.badoo.reaktive.maybe.subscribeOn
|
||||
import com.badoo.reaktive.scheduler.ioScheduler
|
||||
import com.squareup.sqldelight.Query
|
||||
import example.todo.common.database.TodoDatabaseQueries
|
||||
import example.todo.common.database.TodoItemEntity
|
||||
import example.todo.common.database.TodoSharedDatabase
|
||||
import example.todo.common.edit.TodoItem
|
||||
import example.todo.common.edit.store.TodoEditStoreProvider.Database
|
||||
|
||||
internal class TodoEditStoreDatabase(
|
||||
private val queries: TodoDatabaseQueries
|
||||
private val database: TodoSharedDatabase
|
||||
) : Database {
|
||||
|
||||
override fun load(id: Long): Maybe<TodoItem> =
|
||||
maybeFromFunction { queries.select(id = id) }
|
||||
.subscribeOn(ioScheduler)
|
||||
.map(Query<TodoItemEntity>::executeAsOne)
|
||||
.notNull()
|
||||
database
|
||||
.select(id = id)
|
||||
.map { it.toItem() }
|
||||
|
||||
private fun TodoItemEntity.toItem(): TodoItem =
|
||||
@@ -33,10 +24,8 @@ internal class TodoEditStoreDatabase(
|
||||
)
|
||||
|
||||
override fun setText(id: Long, text: String): Completable =
|
||||
completableFromFunction { queries.setText(id = id, text = text) }
|
||||
.subscribeOn(ioScheduler)
|
||||
database.setText(id = id, text = text)
|
||||
|
||||
override fun setDone(id: Long, isDone: Boolean): Completable =
|
||||
completableFromFunction { queries.setDone(id = id, isDone = isDone) }
|
||||
.subscribeOn(ioScheduler)
|
||||
database.setDone(id = id, isDone = isDone)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.arkivanov.decompose.value.operator.map
|
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory
|
||||
import com.badoo.reaktive.base.Consumer
|
||||
import com.badoo.reaktive.base.invoke
|
||||
import example.todo.common.database.TodoSharedDatabase
|
||||
import example.todo.common.main.TodoMain
|
||||
import example.todo.common.main.TodoMain.Model
|
||||
import example.todo.common.main.TodoMain.Output
|
||||
@@ -13,12 +14,11 @@ import example.todo.common.main.store.TodoMainStore.Intent
|
||||
import example.todo.common.main.store.TodoMainStoreProvider
|
||||
import example.todo.common.utils.asValue
|
||||
import example.todo.common.utils.getStore
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
class TodoMainComponent(
|
||||
componentContext: ComponentContext,
|
||||
storeFactory: StoreFactory,
|
||||
database: TodoDatabase,
|
||||
database: TodoSharedDatabase,
|
||||
private val output: Consumer<Output>
|
||||
) : TodoMain, ComponentContext by componentContext {
|
||||
|
||||
@@ -26,7 +26,7 @@ class TodoMainComponent(
|
||||
instanceKeeper.getStore {
|
||||
TodoMainStoreProvider(
|
||||
storeFactory = storeFactory,
|
||||
database = TodoMainStoreDatabase(queries = database.todoDatabaseQueries)
|
||||
database = TodoMainStoreDatabase(database = database)
|
||||
).provide()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,20 @@
|
||||
package example.todo.common.main.integration
|
||||
|
||||
import com.badoo.reaktive.completable.Completable
|
||||
import com.badoo.reaktive.completable.completableFromFunction
|
||||
import com.badoo.reaktive.completable.subscribeOn
|
||||
import com.badoo.reaktive.observable.Observable
|
||||
import com.badoo.reaktive.observable.mapIterable
|
||||
import com.badoo.reaktive.scheduler.ioScheduler
|
||||
import com.squareup.sqldelight.Query
|
||||
import example.todo.common.database.TodoDatabaseQueries
|
||||
import example.todo.common.database.TodoItemEntity
|
||||
import example.todo.common.database.asObservable
|
||||
import example.todo.common.database.TodoSharedDatabase
|
||||
import example.todo.common.main.TodoItem
|
||||
import example.todo.common.main.store.TodoMainStoreProvider
|
||||
|
||||
internal class TodoMainStoreDatabase(
|
||||
private val queries: TodoDatabaseQueries
|
||||
private val database: TodoSharedDatabase
|
||||
) : TodoMainStoreProvider.Database {
|
||||
|
||||
override val updates: Observable<List<TodoItem>> =
|
||||
queries
|
||||
.selectAll()
|
||||
.asObservable(Query<TodoItemEntity>::executeAsList)
|
||||
database
|
||||
.observeAll()
|
||||
.mapIterable { it.toItem() }
|
||||
|
||||
private fun TodoItemEntity.toItem(): TodoItem =
|
||||
@@ -32,14 +26,11 @@ internal class TodoMainStoreDatabase(
|
||||
)
|
||||
|
||||
override fun setDone(id: Long, isDone: Boolean): Completable =
|
||||
completableFromFunction { queries.setDone(id = id, isDone = isDone) }
|
||||
.subscribeOn(ioScheduler)
|
||||
database.setDone(id = id, isDone = isDone)
|
||||
|
||||
override fun delete(id: Long): Completable =
|
||||
completableFromFunction { queries.delete(id = id) }
|
||||
.subscribeOn(ioScheduler)
|
||||
database.delete(id = id)
|
||||
|
||||
override fun add(text: String): Completable =
|
||||
completableFromFunction { queries.add(text = text) }
|
||||
.subscribeOn(ioScheduler)
|
||||
database.add(text = text)
|
||||
}
|
||||
|
||||
@@ -8,12 +8,11 @@ import com.badoo.reaktive.subject.publish.PublishSubject
|
||||
import com.badoo.reaktive.test.observable.assertValue
|
||||
import com.badoo.reaktive.test.observable.test
|
||||
import com.badoo.reaktive.test.scheduler.TestScheduler
|
||||
import example.todo.common.database.TestDatabaseDriver
|
||||
import example.todo.common.database.TestTodoSharedDatabase
|
||||
import example.todo.common.database.TodoItemEntity
|
||||
import example.todo.common.main.TodoItem
|
||||
import example.todo.common.main.TodoMain.Model
|
||||
import example.todo.common.main.TodoMain.Output
|
||||
import example.todo.database.TodoDatabase
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
@@ -25,11 +24,10 @@ import kotlin.test.assertTrue
|
||||
class TodoMainTest {
|
||||
|
||||
private val lifecycle = LifecycleRegistry()
|
||||
private val database = TodoDatabase(TestDatabaseDriver())
|
||||
private val database = TestTodoSharedDatabase(TestScheduler())
|
||||
private val outputSubject = PublishSubject<Output>()
|
||||
private val output = outputSubject.test()
|
||||
|
||||
private val queries = database.todoDatabaseQueries
|
||||
private val databaseTesting = database.testing
|
||||
|
||||
private val impl by lazy {
|
||||
TodoMainComponent(
|
||||
@@ -49,29 +47,29 @@ class TodoMainTest {
|
||||
io = { TestScheduler() }
|
||||
)
|
||||
|
||||
queries.clear()
|
||||
databaseTesting.clear()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun WHEN_item_added_to_database_THEN_item_displayed() {
|
||||
queries.add("Item1")
|
||||
databaseTesting.add("Item1")
|
||||
|
||||
assertEquals("Item1", firstItem().text)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun WHEN_item_deleted_from_database_THEN_item_not_displayed() {
|
||||
queries.add("Item1")
|
||||
databaseTesting.add("Item1")
|
||||
val id = lastInsertItem().id
|
||||
|
||||
queries.delete(id = id)
|
||||
databaseTesting.delete(id = id)
|
||||
|
||||
assertFalse(model.items.any { it.id == id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun WHEN_item_clicked_THEN_Output_Selected_emitted() {
|
||||
queries.add("Item1")
|
||||
databaseTesting.add("Item1")
|
||||
val id = firstItem().id
|
||||
|
||||
impl.onItemClicked(id = id)
|
||||
@@ -81,42 +79,42 @@ class TodoMainTest {
|
||||
|
||||
@Test
|
||||
fun GIVEN_item_isDone_false_WHEN_done_changed_to_true_THEN_item_isDone_true_in_database() {
|
||||
queries.add("Item1")
|
||||
databaseTesting.add("Item1")
|
||||
val id = firstItem().id
|
||||
queries.setDone(id = id, isDone = false)
|
||||
databaseTesting.setDone(id = id, isDone = false)
|
||||
|
||||
impl.onItemDoneChanged(id = id, isDone = true)
|
||||
|
||||
assertTrue(queries.select(id = id).executeAsOne().isDone)
|
||||
assertTrue(databaseTesting.selectRequired(id = id).isDone)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun GIVEN_item_isDone_true_WHEN_done_changed_to_false_THEN_item_isDone_false_in_database() {
|
||||
queries.add("Item1")
|
||||
databaseTesting.add("Item1")
|
||||
val id = firstItem().id
|
||||
queries.setDone(id = id, isDone = true)
|
||||
databaseTesting.setDone(id = id, isDone = true)
|
||||
|
||||
impl.onItemDoneChanged(id = id, isDone = false)
|
||||
|
||||
assertFalse(queries.select(id = id).executeAsOne().isDone)
|
||||
assertFalse(databaseTesting.selectRequired(id = id).isDone)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun WHEN_item_delete_clicked_THEN_item_deleted_in_database() {
|
||||
queries.add("Item1")
|
||||
databaseTesting.add("Item1")
|
||||
val id = firstItem().id
|
||||
|
||||
impl.onItemDeleteClicked(id = id)
|
||||
|
||||
assertNull(queries.select(id = id).executeAsOneOrNull())
|
||||
assertNull(databaseTesting.select(id = id))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun WHEN_item_text_changed_in_database_THEN_item_updated() {
|
||||
queries.add("Item1")
|
||||
databaseTesting.add("Item1")
|
||||
val id = firstItem().id
|
||||
|
||||
queries.setText(id = id, text = "New text")
|
||||
databaseTesting.setText(id = id, text = "New text")
|
||||
|
||||
assertEquals("New text", firstItem().text)
|
||||
}
|
||||
@@ -139,9 +137,6 @@ class TodoMainTest {
|
||||
|
||||
private fun firstItem(): TodoItem = model.items[0]
|
||||
|
||||
private fun lastInsertItem(): TodoItemEntity {
|
||||
val lastInsertId = queries.transactionWithResult<Long> { queries.getLastInsertId().executeAsOne() }
|
||||
|
||||
return queries.select(id = lastInsertId).executeAsOne()
|
||||
}
|
||||
private fun lastInsertItem(): TodoItemEntity =
|
||||
databaseTesting.selectRequired(id = requireNotNull(databaseTesting.getLastInsertId()))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.arkivanov.decompose.statekeeper.Parcelize
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.mvikotlin.core.store.StoreFactory
|
||||
import com.badoo.reaktive.base.Consumer
|
||||
import example.todo.common.database.TodoSharedDatabase
|
||||
import example.todo.common.edit.TodoEdit
|
||||
import example.todo.common.edit.integration.TodoEditComponent
|
||||
import example.todo.common.main.TodoMain
|
||||
@@ -17,7 +18,6 @@ import example.todo.common.main.integration.TodoMainComponent
|
||||
import example.todo.common.root.TodoRoot
|
||||
import example.todo.common.root.TodoRoot.Child
|
||||
import example.todo.common.utils.Consumer
|
||||
import example.todo.database.TodoDatabase
|
||||
|
||||
class TodoRootComponent internal constructor(
|
||||
componentContext: ComponentContext,
|
||||
@@ -28,7 +28,7 @@ class TodoRootComponent internal constructor(
|
||||
constructor(
|
||||
componentContext: ComponentContext,
|
||||
storeFactory: StoreFactory,
|
||||
database: TodoDatabase
|
||||
database: TodoSharedDatabase
|
||||
) : this(
|
||||
componentContext = componentContext,
|
||||
todoMain = { childContext, output ->
|
||||
|
||||
@@ -11,11 +11,11 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen
|
||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
||||
import com.badoo.reaktive.coroutinesinterop.asScheduler
|
||||
import com.badoo.reaktive.scheduler.overrideSchedulers
|
||||
import example.todo.common.database.DefaultTodoSharedDatabase
|
||||
import example.todo.common.database.TodoDatabaseDriver
|
||||
import example.todo.common.root.TodoRoot
|
||||
import example.todo.common.root.integration.TodoRootComponent
|
||||
import example.todo.common.ui.TodoRootContent
|
||||
import example.todo.database.TodoDatabase
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
fun main() {
|
||||
@@ -36,5 +36,5 @@ private fun todoRoot(componentContext: ComponentContext): TodoRoot =
|
||||
TodoRootComponent(
|
||||
componentContext = componentContext,
|
||||
storeFactory = DefaultStoreFactory,
|
||||
database = TodoDatabase(TodoDatabaseDriver())
|
||||
database = DefaultTodoSharedDatabase(TodoDatabaseDriver())
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ struct ContentView: View {
|
||||
TodoRootComponent(
|
||||
componentContext: $0,
|
||||
storeFactory: DefaultStoreFactory(),
|
||||
database: TodoDatabaseCompanion().invoke(driver: TodoDatabaseDriverFactoryKt.TodoDatabaseDriver())
|
||||
database: DefaultTodoSharedDatabase(driver: TodoDatabaseDriverFactoryKt.TodoDatabaseDriver())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,5 +6,6 @@ include(
|
||||
":common:root",
|
||||
":common:compose-ui",
|
||||
":android",
|
||||
":desktop"
|
||||
":desktop",
|
||||
":web"
|
||||
)
|
||||
|
||||
34
examples/todoapp/web/build.gradle.kts
Executable file
34
examples/todoapp/web/build.gradle.kts
Executable file
@@ -0,0 +1,34 @@
|
||||
import org.jetbrains.compose.compose
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
js(IR) {
|
||||
browser {
|
||||
useCommonJs()
|
||||
binaries.executable()
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
named("jsMain") {
|
||||
dependencies {
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.web.widgets)
|
||||
implementation(project(":common:utils"))
|
||||
implementation(project(":common:database"))
|
||||
implementation(project(":common:root"))
|
||||
implementation(project(":common:main"))
|
||||
implementation(project(":common:edit"))
|
||||
implementation(Deps.ArkIvanov.Decompose.decompose)
|
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlin)
|
||||
implementation(Deps.ArkIvanov.MVIKotlin.mvikotlinMain)
|
||||
implementation(npm("copy-webpack-plugin", "9.0.0"))
|
||||
implementation(npm("@material-ui/icons", "4.11.2"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package example.todo.web
|
||||
|
||||
import com.arkivanov.decompose.DefaultComponentContext
|
||||
import com.arkivanov.decompose.lifecycle.LifecycleRegistry
|
||||
import com.arkivanov.decompose.lifecycle.resume
|
||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
||||
import example.todo.common.database.DefaultTodoSharedDatabase
|
||||
import example.todo.common.database.todoDatabaseDriver
|
||||
import example.todo.common.root.integration.TodoRootComponent
|
||||
import kotlinx.browser.document
|
||||
import org.jetbrains.compose.web.css.Style
|
||||
import org.jetbrains.compose.web.renderComposable
|
||||
import org.jetbrains.compose.web.ui.Styles
|
||||
import org.w3c.dom.HTMLElement
|
||||
|
||||
fun main() {
|
||||
val rootElement = document.getElementById("root") as HTMLElement
|
||||
|
||||
val lifecycle = LifecycleRegistry()
|
||||
|
||||
val root =
|
||||
TodoRootComponent(
|
||||
componentContext = DefaultComponentContext(lifecycle = lifecycle),
|
||||
storeFactory = DefaultStoreFactory,
|
||||
database = DefaultTodoSharedDatabase(todoDatabaseDriver())
|
||||
)
|
||||
|
||||
lifecycle.resume()
|
||||
|
||||
renderComposable(root = rootElement) {
|
||||
Style(Styles)
|
||||
|
||||
TodoRootUi(root)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package example.todo.web
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import org.jetbrains.compose.common.material.Text
|
||||
import org.jetbrains.compose.web.attributes.InputType
|
||||
import org.jetbrains.compose.web.attributes.checked
|
||||
import org.jetbrains.compose.web.css.AlignItems
|
||||
import org.jetbrains.compose.web.css.DisplayStyle
|
||||
import org.jetbrains.compose.web.css.JustifyContent
|
||||
import org.jetbrains.compose.web.css.alignItems
|
||||
import org.jetbrains.compose.web.css.display
|
||||
import org.jetbrains.compose.web.css.height
|
||||
import org.jetbrains.compose.web.css.justifyContent
|
||||
import org.jetbrains.compose.web.css.percent
|
||||
import org.jetbrains.compose.web.css.px
|
||||
import org.jetbrains.compose.web.css.width
|
||||
import org.jetbrains.compose.web.dom.A
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.ElementScope
|
||||
import org.jetbrains.compose.web.dom.I
|
||||
import org.jetbrains.compose.web.dom.Input
|
||||
import org.jetbrains.compose.web.dom.Label
|
||||
import org.jetbrains.compose.web.dom.Li
|
||||
import org.jetbrains.compose.web.dom.Nav
|
||||
import org.jetbrains.compose.web.dom.Span
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
import org.jetbrains.compose.web.dom.TextArea
|
||||
import org.jetbrains.compose.web.dom.Ul
|
||||
import org.w3c.dom.HTMLUListElement
|
||||
|
||||
@Composable
|
||||
fun MaterialCheckbox(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
attrs: AttrBuilderContext<*> = {},
|
||||
content: @Composable () -> Unit = {}
|
||||
) {
|
||||
Div(attrs = attrs) {
|
||||
Label {
|
||||
Input(
|
||||
type = InputType.Checkbox,
|
||||
attrs = {
|
||||
classes("filled-in")
|
||||
if (checked) checked()
|
||||
onCheckboxInput { onCheckedChange(it.checked) }
|
||||
}
|
||||
)
|
||||
|
||||
Span {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Card(attrs: AttrBuilderContext<*> = {}, content: @Composable () -> Unit) {
|
||||
Div(
|
||||
attrs = {
|
||||
classes("card")
|
||||
attrs()
|
||||
}
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MaterialTextArea(
|
||||
id: String,
|
||||
label: String,
|
||||
text: String,
|
||||
onTextChanged: (String) -> Unit,
|
||||
attrs: AttrBuilderContext<*> = {}
|
||||
) {
|
||||
Div(
|
||||
attrs = {
|
||||
classes("input-field", "col", "s12")
|
||||
attrs()
|
||||
}
|
||||
) {
|
||||
TextArea(
|
||||
value = text,
|
||||
attrs = {
|
||||
id("text_area_add_todo")
|
||||
classes("materialize-textarea")
|
||||
onTextInput { onTextChanged(it.inputValue) }
|
||||
style {
|
||||
width(100.percent)
|
||||
height(100.percent)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Label(forId = id) {
|
||||
Text(text = label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageButton(
|
||||
onClick: () -> Unit,
|
||||
iconName: String,
|
||||
attrs: AttrBuilderContext<*> = {}
|
||||
) {
|
||||
A(
|
||||
attrs = {
|
||||
classes("waves-effect", "waves-teal", "btn-flat")
|
||||
style {
|
||||
width(48.px)
|
||||
height(48.px)
|
||||
display(DisplayStyle.Flex)
|
||||
alignItems(AlignItems.Center)
|
||||
justifyContent(JustifyContent.Center)
|
||||
}
|
||||
this.onClick { onClick() }
|
||||
attrs()
|
||||
}
|
||||
) {
|
||||
MaterialIcon(name = iconName)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MaterialIcon(name: String) {
|
||||
I(attrs = { classes("material-icons") }) { Text(value = name) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavBar(
|
||||
title: String,
|
||||
navigationIcon: NavBarIcon? = null
|
||||
) {
|
||||
Nav {
|
||||
Div(attrs = { classes("nav-wrapper") }) {
|
||||
if (navigationIcon != null) {
|
||||
Ul(attrs = { classes("left") }) {
|
||||
NavBarIcon(icon = navigationIcon)
|
||||
}
|
||||
}
|
||||
|
||||
A(
|
||||
attrs = {
|
||||
classes("brand-logo")
|
||||
style {
|
||||
property("padding-left", 16.px)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(value = title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ElementScope<HTMLUListElement>.NavBarIcon(icon: NavBarIcon) {
|
||||
Li {
|
||||
A(
|
||||
attrs = {
|
||||
onClick { icon.onClick() }
|
||||
}
|
||||
) {
|
||||
MaterialIcon(name = icon.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NavBarIcon(
|
||||
val name: String,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
@@ -0,0 +1,115 @@
|
||||
package example.todo.web
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import kotlinx.coroutines.delay
|
||||
import org.jetbrains.compose.web.css.Position
|
||||
import org.jetbrains.compose.web.css.height
|
||||
import org.jetbrains.compose.web.css.left
|
||||
import org.jetbrains.compose.web.css.opacity
|
||||
import org.jetbrains.compose.web.css.percent
|
||||
import org.jetbrains.compose.web.css.position
|
||||
import org.jetbrains.compose.web.css.px
|
||||
import org.jetbrains.compose.web.css.top
|
||||
import org.jetbrains.compose.web.css.width
|
||||
import org.jetbrains.compose.web.dom.AttrBuilderContext
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import kotlin.js.Date
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@Composable
|
||||
fun <T : Any> Crossfade(target: T, attrs: AttrBuilderContext<*> = {}, content: @Composable (T) -> Unit) {
|
||||
val holder = remember { TargetHolder<T>(null) }
|
||||
val previousTarget: T? = holder.value
|
||||
|
||||
if (previousTarget == null) {
|
||||
holder.value = target
|
||||
Div(attrs = attrs) {
|
||||
content(target)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (previousTarget == target) {
|
||||
Div(attrs = attrs) {
|
||||
content(target)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
holder.value = target
|
||||
|
||||
val animationFactor by animateFloatFactor(key = target, durationMillis = 300L, easing = ::sqrt)
|
||||
|
||||
Div(attrs = attrs) {
|
||||
if (animationFactor < 1F) {
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
height(100.percent)
|
||||
position(Position.Absolute)
|
||||
top(0.px)
|
||||
left(0.px)
|
||||
opacity(1F - animationFactor)
|
||||
}
|
||||
}
|
||||
) {
|
||||
content(previousTarget)
|
||||
}
|
||||
}
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
height(100.percent)
|
||||
position(Position.Absolute)
|
||||
top(0.px)
|
||||
left(0.px)
|
||||
if (animationFactor < 1F) {
|
||||
opacity(animationFactor)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
content(target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TargetHolder<T : Any>(
|
||||
var value: T?
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun animateFloatFactor(key: Any, durationMillis: Long, easing: Easing = Easing { it }): State<Float> {
|
||||
val state = remember(key) { mutableStateOf(0F) }
|
||||
|
||||
LaunchedEffect(key) {
|
||||
var date = Date.now()
|
||||
val startMillis = date
|
||||
val endMillis = startMillis + durationMillis.toDouble()
|
||||
while (true) {
|
||||
date = Date.now()
|
||||
if (date >= endMillis) {
|
||||
break
|
||||
}
|
||||
|
||||
state.value = easing.transform(((date - startMillis) / durationMillis.toDouble()).toFloat())
|
||||
delay(16L)
|
||||
}
|
||||
|
||||
state.value = 1F
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
private fun interface Easing {
|
||||
fun transform(fraction: Float): Float
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package example.todo.web
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import example.todo.common.edit.TodoEdit
|
||||
import org.jetbrains.compose.web.css.DisplayStyle
|
||||
import org.jetbrains.compose.web.css.FlexDirection
|
||||
import org.jetbrains.compose.web.css.FlexWrap
|
||||
import org.jetbrains.compose.web.css.JustifyContent
|
||||
import org.jetbrains.compose.web.css.display
|
||||
import org.jetbrains.compose.web.css.flexFlow
|
||||
import org.jetbrains.compose.web.css.height
|
||||
import org.jetbrains.compose.web.css.justifyContent
|
||||
import org.jetbrains.compose.web.css.percent
|
||||
import org.jetbrains.compose.web.css.width
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
|
||||
@Composable
|
||||
fun TodoEditUi(component: TodoEdit) {
|
||||
val model by component.models.subscribeAsState()
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
height(100.percent)
|
||||
display(DisplayStyle.Flex)
|
||||
flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
property("flex", "0 1 auto")
|
||||
}
|
||||
}
|
||||
) {
|
||||
NavBar(
|
||||
title = "Edit todo",
|
||||
navigationIcon = NavBarIcon(
|
||||
name = "arrow_back",
|
||||
onClick = component::onCloseClicked
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
property("flex", "1 1 auto")
|
||||
property("padding", "0px 16px 0px 16px")
|
||||
display(DisplayStyle.Flex)
|
||||
flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
|
||||
}
|
||||
}
|
||||
) {
|
||||
MaterialTextArea(
|
||||
id = "text_area_edit_todo",
|
||||
label = "",
|
||||
text = model.text,
|
||||
onTextChanged = component::onTextChanged,
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
property("flex", "1 1 auto")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
property("flex", "0 1 auto")
|
||||
property("padding-bottom", "16px")
|
||||
display(DisplayStyle.Flex)
|
||||
justifyContent(JustifyContent.Center)
|
||||
}
|
||||
}
|
||||
) {
|
||||
MaterialCheckbox(
|
||||
checked = model.isDone,
|
||||
onCheckedChange = component::onDoneChanged,
|
||||
content = {
|
||||
Text(value = "Completed")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package example.todo.web
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import example.todo.common.main.TodoItem
|
||||
import example.todo.common.main.TodoMain
|
||||
import org.jetbrains.compose.web.css.AlignItems
|
||||
import org.jetbrains.compose.web.css.DisplayStyle
|
||||
import org.jetbrains.compose.web.css.FlexDirection
|
||||
import org.jetbrains.compose.web.css.FlexWrap
|
||||
import org.jetbrains.compose.web.css.alignItems
|
||||
import org.jetbrains.compose.web.css.display
|
||||
import org.jetbrains.compose.web.css.flexFlow
|
||||
import org.jetbrains.compose.web.css.height
|
||||
import org.jetbrains.compose.web.css.margin
|
||||
import org.jetbrains.compose.web.css.marginLeft
|
||||
import org.jetbrains.compose.web.css.percent
|
||||
import org.jetbrains.compose.web.css.px
|
||||
import org.jetbrains.compose.web.css.width
|
||||
import org.jetbrains.compose.web.dom.DOMScope
|
||||
import org.jetbrains.compose.web.dom.Div
|
||||
import org.jetbrains.compose.web.dom.Li
|
||||
import org.jetbrains.compose.web.dom.Text
|
||||
import org.jetbrains.compose.web.dom.Ul
|
||||
import org.w3c.dom.HTMLUListElement
|
||||
|
||||
|
||||
@Composable
|
||||
fun TodoMainUi(component: TodoMain) {
|
||||
val model by component.models.subscribeAsState()
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
height(100.percent)
|
||||
display(DisplayStyle.Flex)
|
||||
flexFlow(FlexDirection.Column, FlexWrap.Nowrap)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
property("flex", "0 1 auto")
|
||||
}
|
||||
}
|
||||
) {
|
||||
NavBar(title = "Todo List")
|
||||
}
|
||||
|
||||
Ul(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
margin(0.px)
|
||||
property("flex", "1 1 auto")
|
||||
property("overflow-y", "scroll")
|
||||
}
|
||||
}
|
||||
) {
|
||||
model.items.forEach { item ->
|
||||
Item(
|
||||
item = item,
|
||||
onClicked = component::onItemClicked,
|
||||
onDoneChanged = component::onItemDoneChanged,
|
||||
onDeleteClicked = component::onItemDeleteClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
property("flex", "0 1 auto")
|
||||
}
|
||||
}
|
||||
) {
|
||||
TodoInput(
|
||||
text = model.text,
|
||||
onTextChanged = component::onInputTextChanged,
|
||||
onAddClicked = component::onAddItemClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DOMScope<HTMLUListElement>.Item(
|
||||
item: TodoItem,
|
||||
onClicked: (id: Long) -> Unit,
|
||||
onDoneChanged: (id: Long, isDone: Boolean) -> Unit,
|
||||
onDeleteClicked: (id: Long) -> Unit
|
||||
) {
|
||||
Li(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
display(DisplayStyle.Flex)
|
||||
flexFlow(FlexDirection.Row, FlexWrap.Nowrap)
|
||||
alignItems(AlignItems.Center)
|
||||
property("padding", "0px 0px 0px 16px")
|
||||
}
|
||||
}
|
||||
) {
|
||||
MaterialCheckbox(
|
||||
checked = item.isDone,
|
||||
onCheckedChange = { onDoneChanged(item.id, !item.isDone) },
|
||||
attrs = {
|
||||
style {
|
||||
property("flex", "0 1 auto")
|
||||
property("padding-top", 10.px) // Fix for the checkbox not being centered vertically
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
height(48.px)
|
||||
property("flex", "1 1 auto")
|
||||
property("white-space", "nowrap")
|
||||
property("text-overflow", "ellipsis")
|
||||
property("overflow", "hidden")
|
||||
display(DisplayStyle.Flex)
|
||||
alignItems(AlignItems.Center)
|
||||
}
|
||||
onClick { onClicked(item.id) }
|
||||
}
|
||||
) {
|
||||
Text(value = item.text)
|
||||
}
|
||||
|
||||
ImageButton(
|
||||
onClick = { onDeleteClicked(item.id) },
|
||||
iconName = "delete",
|
||||
attrs = {
|
||||
style {
|
||||
property("flex", "0 1 auto")
|
||||
marginLeft(8.px)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TodoInput(
|
||||
text: String,
|
||||
onTextChanged: (String) -> Unit,
|
||||
onAddClicked: () -> Unit
|
||||
) {
|
||||
Div(
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
display(DisplayStyle.Flex)
|
||||
flexFlow(FlexDirection.Row, FlexWrap.Nowrap)
|
||||
alignItems(AlignItems.Center)
|
||||
}
|
||||
}
|
||||
) {
|
||||
MaterialTextArea(
|
||||
id = "text_area_add_todo",
|
||||
label = "Add todo",
|
||||
text = text,
|
||||
onTextChanged = onTextChanged,
|
||||
attrs = {
|
||||
style {
|
||||
property("flex", "1 1 auto")
|
||||
margin(16.px)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ImageButton(
|
||||
onClick = onAddClicked,
|
||||
iconName = "add",
|
||||
attrs = {
|
||||
style {
|
||||
property("flex", "0 1 auto")
|
||||
property("margin", "0px 16px 0px 0px")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package example.todo.web
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import example.todo.common.root.TodoRoot
|
||||
import org.jetbrains.compose.web.css.Position
|
||||
import org.jetbrains.compose.web.css.auto
|
||||
import org.jetbrains.compose.web.css.bottom
|
||||
import org.jetbrains.compose.web.css.height
|
||||
import org.jetbrains.compose.web.css.left
|
||||
import org.jetbrains.compose.web.css.percent
|
||||
import org.jetbrains.compose.web.css.position
|
||||
import org.jetbrains.compose.web.css.px
|
||||
import org.jetbrains.compose.web.css.right
|
||||
import org.jetbrains.compose.web.css.top
|
||||
import org.jetbrains.compose.web.css.width
|
||||
|
||||
@Composable
|
||||
fun TodoRootUi(component: TodoRoot) {
|
||||
Card(
|
||||
attrs = {
|
||||
style {
|
||||
position(Position.Absolute)
|
||||
height(700.px)
|
||||
property("max-width", 640.px)
|
||||
top(0.px)
|
||||
bottom(0.px)
|
||||
left(0.px)
|
||||
right(0.px)
|
||||
property("margin", auto)
|
||||
}
|
||||
}
|
||||
) {
|
||||
val routerState by component.routerState.subscribeAsState()
|
||||
|
||||
Crossfade(
|
||||
target = routerState.activeChild.instance,
|
||||
attrs = {
|
||||
style {
|
||||
width(100.percent)
|
||||
height(100.percent)
|
||||
position(Position.Relative)
|
||||
left(0.px)
|
||||
top(0.px)
|
||||
}
|
||||
}
|
||||
) { child ->
|
||||
when (child) {
|
||||
is TodoRoot.Child.Main -> TodoMainUi(child.component)
|
||||
is TodoRoot.Child.Edit -> TodoEditUi(child.component)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package example.todo.web
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import com.arkivanov.decompose.value.Value
|
||||
import com.arkivanov.decompose.value.ValueObserver
|
||||
|
||||
@Composable
|
||||
fun <T : Any> Value<T>.subscribeAsState(): State<T> {
|
||||
val state = remember(this) { mutableStateOf(value) }
|
||||
|
||||
DisposableEffect(this) {
|
||||
val observer: ValueObserver<T> = { state.value = it }
|
||||
|
||||
subscribe(observer)
|
||||
|
||||
onDispose {
|
||||
unsubscribe(observer)
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
Binary file not shown.
30
examples/todoapp/web/src/jsMain/resources/index.html
Normal file
30
examples/todoapp/web/src/jsMain/resources/index.html
Normal file
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
~ Copyright 2021 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Todo</title>
|
||||
<link href="styles.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="materialize.min.css" media="screen,projection" rel="stylesheet" type="text/css"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="web.js"></script>
|
||||
<script src="materialize.min.js" type="text/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
13
examples/todoapp/web/src/jsMain/resources/materialize.min.css
vendored
Normal file
13
examples/todoapp/web/src/jsMain/resources/materialize.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
examples/todoapp/web/src/jsMain/resources/materialize.min.js
vendored
Normal file
6
examples/todoapp/web/src/jsMain/resources/materialize.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
34
examples/todoapp/web/src/jsMain/resources/styles.css
Normal file
34
examples/todoapp/web/src/jsMain/resources/styles.css
Normal file
@@ -0,0 +1,34 @@
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px; /* Preferred icon size */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
||||
|
||||
9
examples/todoapp/web/webpack.config.d/fs.js
Normal file
9
examples/todoapp/web/webpack.config.d/fs.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// As per https://cashapp.github.io/sqldelight/js_sqlite/
|
||||
|
||||
config.resolve = {
|
||||
fallback: {
|
||||
fs: false,
|
||||
path: false,
|
||||
crypto: false
|
||||
}
|
||||
}
|
||||
12
examples/todoapp/web/webpack.config.d/wasm.js
Normal file
12
examples/todoapp/web/webpack.config.d/wasm.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// As per https://cashapp.github.io/sqldelight/js_sqlite/
|
||||
|
||||
var CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
config.plugins.push(
|
||||
new CopyWebpackPlugin(
|
||||
{
|
||||
patterns: [
|
||||
{from: '../../node_modules/sql.js/dist/sql-wasm.wasm', to: '../../../web/build/distributions'}
|
||||
]
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user