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