Skip to content

Commit

Permalink
Add web example for JS target.
Browse files Browse the repository at this point in the history
  • Loading branch information
christiandeange committed Nov 24, 2023
1 parent 83a4b23 commit 9b31517
Show file tree
Hide file tree
Showing 59 changed files with 3,701 additions and 147 deletions.
7 changes: 6 additions & 1 deletion app/android/src/main/java/sh/christian/ozone/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import dev.marcellogalhardo.retained.activity.retain
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import sh.christian.ozone.di.AppComponent
Expand Down Expand Up @@ -41,12 +42,16 @@ class MainActivity : AppCompatActivity() {
initTypography()
}

val authInfo = runBlocking {
appComponent.loginRepository.auth().first()
}

setContent {
AppTheme {
StatusBarTheme()
WorkflowRendering(
workflow = workflow,
props = Unit,
props = authInfo,
onOutput = { finish() },
content = { it.Content() },
)
Expand Down
20 changes: 15 additions & 5 deletions app/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ kotlin {
api(compose.material3)
api(compose.runtime)

api(libs.kotlinx.serialization.core)
api(libs.workflow.core)
api(libs.workflow.runtime)

Expand All @@ -28,9 +29,8 @@ kotlin {
implementation(libs.kamel)
implementation(libs.kotlininject)
implementation(libs.kotlinx.atomicfu)
implementation(libs.ktor.cio)
implementation(libs.kotlinx.coroutines)
implementation(libs.ktor.logging)
implementation(libs.zoomable)

api(project(":bluesky"))
api(project(":app:store"))
Expand All @@ -44,17 +44,26 @@ kotlin {

dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.ktor.cio)
implementation(libs.zoomable)
}
}
val desktopMain by getting {
kotlin.srcDir("build/generated/ksp/desktop/desktopMain/kotlin")
resources.srcDir("fonts")

sourceSets {
resources.srcDir("fonts")
dependencies {
implementation(libs.apache.commons)
implementation(libs.ktor.cio)
implementation(libs.zoomable)
}
}
val jsMain by getting {
kotlin.srcDir("build/generated/ksp/js/jsMain/kotlin")
resources.srcDir("fonts")

dependencies {
implementation(libs.apache.commons)
implementation(libs.ktor.js)
}
}
}
Expand All @@ -63,4 +72,5 @@ kotlin {
dependencies {
add("kspAndroid", libs.kotlininject.compiler)
add("kspDesktop", libs.kotlininject.compiler)
add("kspJs", libs.kotlininject.compiler)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sh.christian.ozone.api

import io.ktor.client.engine.HttpClientEngineFactory
import io.ktor.client.engine.cio.CIO
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

actual val engine: HttpClientEngineFactory<*> get() = CIO

actual object OzoneDispatchers {
actual val IO: CoroutineDispatcher get() = Dispatchers.IO
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import kotlin.streams.toList

lateinit var fontsAssetManager: AssetManager

Expand Down Expand Up @@ -33,3 +34,11 @@ actual suspend fun appFont(): FontFamily {
}
)
}

actual fun String.codepoints(): List<Int> {
return codePoints().toList()
}

actual fun String.codePointsCount(): Int {
return codePointCount(0, length)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package sh.christian.ozone.ui.compose

import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import me.saket.telephoto.zoomable.ZoomableContentLocation
import me.saket.telephoto.zoomable.rememberZoomableState
import me.saket.telephoto.zoomable.zoomable

@Composable
actual fun ZoomableImage(
painter: Painter,
contentDescription: String?,
modifier: Modifier,
alignment: Alignment,
contentScale: ContentScale,
alpha: Float,
colorFilter: ColorFilter?,
): ZoomableImageHandle {
val zoomableState = rememberZoomableState()

Image(
modifier = modifier.zoomable(zoomableState),
painter = painter,
contentDescription = contentDescription,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
)

LaunchedEffect(painter) {
zoomableState.setContentLocation(
ZoomableContentLocation.scaledInsideAndCenterAligned(painter.intrinsicSize)
)
}

return remember(zoomableState) {
ZoomableImageHandle(resetZoom = {
zoomableState.resetZoom(withAnimation = false)
})
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package sh.christian.ozone.api

import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.DefaultRequest
import io.ktor.client.plugins.logging.DEFAULT
import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger
import io.ktor.client.plugins.logging.Logging
import io.ktor.http.Url
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -31,24 +29,24 @@ import sh.christian.ozone.login.auth.AuthInfo
class ApiProvider(
private val apiRepository: ServerRepository,
private val loginRepository: LoginRepository,
) : Supervisor {
) : Supervisor() {

private val apiHost = MutableStateFlow(apiRepository.server!!.host)
private val auth = MutableStateFlow(loginRepository.auth)
private val tokens = MutableStateFlow(loginRepository.auth?.toTokens())
private val apiHost = MutableStateFlow<String?>(null)
private val auth = MutableStateFlow<AuthInfo?>(null)
private val tokens = MutableStateFlow<Tokens?>(null)

private val client = HttpClient(CIO) {
private val client = HttpClient(engine) {
install(Logging) {
logger = Logger.DEFAULT
level = LogLevel.INFO
level = LogLevel.NONE
}

install(XrpcAuthPlugin) {
authTokens = tokens
}

install(DefaultRequest) {
val hostUrl = Url(apiHost.value)
val hostUrl = Url(apiHost.value!!)
url.protocol = hostUrl.protocol
url.host = hostUrl.host
url.port = hostUrl.port
Expand All @@ -61,13 +59,13 @@ class ApiProvider(

override suspend fun CoroutineScope.onStart() {
coroutineScope {
launch(Dispatchers.IO) {
launch(OzoneDispatchers.IO) {
apiRepository.server().map { it.host }
.distinctUntilChanged()
.collect(apiHost)
}

launch(Dispatchers.IO) {
launch(OzoneDispatchers.IO) {
loginRepository.auth()
.distinctUntilChanged()
.collect {
Expand All @@ -77,12 +75,12 @@ class ApiProvider(
}
}

launch(Dispatchers.IO) {
launch(OzoneDispatchers.IO) {
tokens.collect { tokens ->
if (tokens != null) {
loginRepository.auth = loginRepository.auth().first()!!.withTokens(tokens)
loginRepository.setAuth(loginRepository.auth().first()!!.withTokens(tokens))
} else {
loginRepository.auth = null
loginRepository.setAuth(null)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package sh.christian.ozone.api

import com.squareup.workflow1.Worker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
Expand All @@ -10,7 +9,7 @@ import sh.christian.ozone.api.response.AtpResponse
abstract class NetworkWorker<T> : Worker<AtpResponse<T>> {
override fun run(): Flow<AtpResponse<T>> = flow {
emit(execute())
}.flowOn(Dispatchers.IO)
}.flowOn(OzoneDispatchers.IO)

abstract suspend fun execute(): AtpResponse<T>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,26 @@ package sh.christian.ozone.api

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.launch
import me.tatarka.inject.annotations.Inject
import sh.christian.ozone.app.Supervisor
import sh.christian.ozone.di.SingleInApp
import sh.christian.ozone.login.auth.Server
import sh.christian.ozone.store.PersistentStorage
import sh.christian.ozone.store.getValue
import sh.christian.ozone.store.preference
import sh.christian.ozone.store.setValue

@Inject
@SingleInApp
class ServerRepository(
storage: PersistentStorage,
) {
) : Supervisor() {
private val serverPreference = storage.preference<Server>("servers", Server.BlueskySocial)

var server by serverPreference
fun setServer(server: Server) {
requireCoroutineScope().launch {
serverPreference.set(server)
}
}

fun server(): Flow<Server> = serverPreference.updates.filterNotNull()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package sh.christian.ozone.api

import io.ktor.client.engine.HttpClientEngineFactory
import kotlinx.coroutines.CoroutineDispatcher

expect val engine: HttpClientEngineFactory<*>

expect object OzoneDispatchers {
val IO: CoroutineDispatcher
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import com.squareup.workflow1.action
import com.squareup.workflow1.asWorker
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.runningWorker
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import me.tatarka.inject.annotations.Inject
import sh.christian.ozone.app.AppState.ShowingLoggedIn
import sh.christian.ozone.app.AppState.ShowingLogin
Expand All @@ -18,6 +18,7 @@ import sh.christian.ozone.login.LoginOutput.CanceledLogin
import sh.christian.ozone.login.LoginOutput.LoggedIn
import sh.christian.ozone.login.LoginRepository
import sh.christian.ozone.login.LoginWorkflow
import sh.christian.ozone.login.auth.AuthInfo
import sh.christian.ozone.notifications.NotificationsRepository

@Inject
Expand All @@ -26,33 +27,34 @@ class AppWorkflow(
private val loginWorkflow: LoginWorkflow,
private val homeWorkflow: HomeWorkflow,
private val notificationsRepository: NotificationsRepository,
) : StatefulWorkflow<Unit, AppState, Unit, AppScreen>() {
) : StatefulWorkflow<AuthInfo?, AppState, Unit, AppScreen>() {
override fun initialState(
props: Unit,
props: AuthInfo?,
snapshot: Snapshot?,
): AppState {
val authInfo = runBlocking { loginRepository.auth().first() }
return if (authInfo == null) {
return if (props == null) {
ShowingLogin
} else {
ShowingLoggedIn(HomeProps(authInfo, 0))
ShowingLoggedIn(HomeProps(props, 0))
}
}

override fun render(
renderProps: Unit,
renderProps: AuthInfo?,
renderState: AppState,
context: RenderContext,
): AppScreen = when (renderState) {
is ShowingLogin -> {
context.runningWorker(loginRepository.auth().filterNotNull().asWorker(), "has-auth") { auth ->
action {
state = ShowingLoggedIn(HomeProps(auth, 0))
}
}

context.renderChild(loginWorkflow) { output ->
action {
when (output) {
is LoggedIn -> {
loginRepository.auth = output.authInfo
state = ShowingLoggedIn(HomeProps(output.authInfo, 0))
}

is LoggedIn -> loginRepository.setAuth(output.authInfo)
is CanceledLogin -> setOutput(Unit)
}
}
Expand All @@ -64,15 +66,17 @@ class AppWorkflow(
state = ShowingLoggedIn(renderState.props.copy(unreadNotificationCount = unread))
}
}
context.runningWorker(loginRepository.auth().filter { it == null }.asWorker(), "no-auth") {
action {
state = ShowingLogin
}
}

context.renderChild(homeWorkflow, renderState.props) { output ->
action {
when (output) {
is HomeOutput.CloseApp -> setOutput(Unit)
is HomeOutput.SignOut -> {
loginRepository.auth = null
state = ShowingLogin
}
is HomeOutput.SignOut -> loginRepository.setAuth(null)
}
}
}
Expand Down
Loading

0 comments on commit 9b31517

Please sign in to comment.