Browse Source
* 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.pull/796/head
Arkadii Ivanov
3 years ago
committed by
GitHub
38 changed files with 1124 additions and 160 deletions
@ -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() |
@ -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.
@ -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> |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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'; |
||||||
|
} |
||||||
|
|
@ -0,0 +1,9 @@ |
|||||||
|
// As per https://cashapp.github.io/sqldelight/js_sqlite/
|
||||||
|
|
||||||
|
config.resolve = { |
||||||
|
fallback: { |
||||||
|
fs: false, |
||||||
|
path: false, |
||||||
|
crypto: false |
||||||
|
} |
||||||
|
} |
@ -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'} |
||||||
|
] |
||||||
|
} |
||||||
|
) |
||||||
|
); |
Loading…
Reference in new issue