diff options
| author | Guillermo Serrahima <dev@vylion.com> | 2026-10-15 02:43:39 +0200 |
|---|---|---|
| committer | Guillermo Serrahima <dev@vylion.com> | 2026-10-15 02:43:39 +0200 |
| commit | 785492400072f8f5f10d67ad295befadb98f7baf (patch) | |
| tree | a3d55ff08a1483ee91d634d06cd4fee874b978ae /shared | |
| download | lluerna-785492400072f8f5f10d67ad295befadb98f7baf.tar.xz lluerna-785492400072f8f5f10d67ad295befadb98f7baf.zip | |
Diffstat (limited to 'shared')
8 files changed, 206 insertions, 0 deletions
diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts new file mode 100644 index 0000000..8b56841 --- /dev/null +++ b/shared/build.gradle.kts @@ -0,0 +1,46 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidMultiplatformLibrary) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.kotlinSerialization) +} + +// :shared is a kotlin multiplatform *library*. it holds the domain model, the +// ui, and (later) networking. the runnable apps live in :androidApp / :desktopApp. +// +// with agp 9 this module uses com.android.kotlin.multiplatform.library, not +// com.android.application/com.android.library, and there is no top-level +// `android {}` block — android config lives in `kotlin { androidLibrary {} }`. +kotlin { + android { + namespace = "dev.serrahima.lluerna.shared" + compileSdk = libs.versions.android.compileSdk.get().toInt() + minSdk = libs.versions.android.minSdk.get().toInt() + + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + + // off by default in the new plugin; needed if we ever use compose resources. + androidResources { + enable = true + } + } + + // the jvm target, consumed by :desktopApp. + jvm() + + sourceSets { + commonMain.dependencies { + implementation(libs.compose.runtime) + implementation(libs.compose.foundation) + implementation(libs.compose.material3) + implementation(libs.compose.ui) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.serialization.json) + } + } +} diff --git a/shared/src/androidMain/kotlin/dev/serrahima/lluerna/Platform.android.kt b/shared/src/androidMain/kotlin/dev/serrahima/lluerna/Platform.android.kt new file mode 100644 index 0000000..10877a0 --- /dev/null +++ b/shared/src/androidMain/kotlin/dev/serrahima/lluerna/Platform.android.kt @@ -0,0 +1,6 @@ +package dev.serrahima.lluerna + +import android.os.Build + +actual fun platformName(): String = + "android ${Build.VERSION.RELEASE} (api ${Build.VERSION.SDK_INT})" diff --git a/shared/src/commonMain/kotlin/dev/serrahima/lluerna/App.kt b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/App.kt new file mode 100644 index 0000000..9cad551 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/App.kt @@ -0,0 +1,36 @@ +package dev.serrahima.lluerna + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.serrahima.lluerna.ui.LluernaTheme + +/** + * the one entrypoint both platforms render. + * + * android calls this from MainActivity; desktop calls it from the Window. + * everything below here is shared code. + */ +@Composable +fun App() { + LluernaTheme { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.fillMaxSize().padding(24.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text("lluerna", style = MaterialTheme.typography.headlineMedium) + Text("running on ${platformName()}") + } + } + } +} diff --git a/shared/src/commonMain/kotlin/dev/serrahima/lluerna/Platform.kt b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/Platform.kt new file mode 100644 index 0000000..aa04fa3 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/Platform.kt @@ -0,0 +1,7 @@ +package dev.serrahima.lluerna + +/** + * `expect` declares a function that each platform must provide an `actual` for. + * it is kotlin multiplatform's core mechanism for "this differs per platform". + */ +expect fun platformName(): String diff --git a/shared/src/commonMain/kotlin/dev/serrahima/lluerna/domain/Model.kt b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/domain/Model.kt new file mode 100644 index 0000000..680bfcf --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/domain/Model.kt @@ -0,0 +1,74 @@ +package dev.serrahima.lluerna.domain + +/** + * the protocol-neutral domain model. + * + * nothing in here knows about `site.standard.*` (or any other lexicon). the + * source adapters map raw records into these types; the ui renders these types. + * that boundary is what makes a future bluesky/wafrn adapter additive. + * + * this is a starter: it will grow as we build the feed. + */ + +/** which protocol a piece of content came from. */ +enum class Protocol { STANDARD_SITE } + +/** a publication (ours) or a foreign actor, depending on [protocol]. */ +data class Author( + /** did, or an at-uri for a publication. */ + val id: String, + /** a handle or publication slug, if known. */ + val handle: String? = null, + val name: String, + val avatarUrl: String? = null, + /** canonical web url, if the author has one. */ + val url: String? = null, + val protocol: Protocol = Protocol.STANDARD_SITE, +) + +/** + * a post body is a sum type: either our rich native content, or opaque foreign + * content we can only render textually (a future bluesky/wafrn post). + */ +sealed interface PostBody { + data class Native(val blocks: List<Block>) : PostBody + data class Foreign(val text: String, val facets: List<Facet> = emptyList()) : PostBody +} + +/** a block of native rich content. will grow (images, embeds, ...). */ +sealed interface Block { + data class Paragraph(val text: String, val facets: List<Facet> = emptyList()) : Block + data class Header(val level: Int, val text: String) : Block +} + +/** an inline annotation over a byte range of a text. */ +sealed interface Facet { + val byteStart: Int + val byteEnd: Int + + data class Link(override val byteStart: Int, override val byteEnd: Int, val uri: String) : Facet + data class Mention(override val byteStart: Int, override val byteEnd: Int, val uri: String) : Facet + data class Bold(override val byteStart: Int, override val byteEnd: Int) : Facet + data class Italic(override val byteStart: Int, override val byteEnd: Int) : Facet + data class Header(override val byteStart: Int, override val byteEnd: Int, val level: Int) : Facet +} + +/** + * a post in the feed. [parents] is the chain toward the root, newest-nearest + * first; the tumblr dedup + "tags from every post in the chain" logic both + * operate on it. + */ +data class Post( + val uri: String, + val cid: String, + val author: Author, + val body: PostBody, + /** plaintext fallback (site.standard.document.textContent). */ + val plaintext: String, + val tags: List<String> = emptyList(), + val publishedAt: Long, + val updatedAt: Long? = null, + val parents: List<Post> = emptyList(), + /** at-uri of the post this is an addition to, if it is a reblog-with-addition. */ + val replyTarget: String? = null, +) diff --git a/shared/src/commonMain/kotlin/dev/serrahima/lluerna/source/PostSource.kt b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/source/PostSource.kt new file mode 100644 index 0000000..fbf11bc --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/source/PostSource.kt @@ -0,0 +1,20 @@ +package dev.serrahima.lluerna.source + +import dev.serrahima.lluerna.domain.Post + +/** + * the seam that keeps the ui protocol-agnostic. + * + * v1 implements exactly one source (`StandardSiteSource`). a future + * `BlueskySource` / `WafrnSource` implements this same interface; the ui never + * learns a second protocol. + * + * (shape is tentative — it will settle once the feed milestone lands.) + */ +interface PostSource { + /** a follow-based feed for the given followed publication ids (at-uris). */ + suspend fun feed(followedPublications: List<String>): List<Post> + + /** the full chain ending at [uri], root-first. */ + suspend fun thread(uri: String): List<Post> +} diff --git a/shared/src/commonMain/kotlin/dev/serrahima/lluerna/ui/LluernaTheme.kt b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/ui/LluernaTheme.kt new file mode 100644 index 0000000..7b395a3 --- /dev/null +++ b/shared/src/commonMain/kotlin/dev/serrahima/lluerna/ui/LluernaTheme.kt @@ -0,0 +1,13 @@ +package dev.serrahima.lluerna.ui + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable + +@Composable +fun LluernaTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = darkColorScheme(), + content = content, + ) +} diff --git a/shared/src/jvmMain/kotlin/dev/serrahima/lluerna/Platform.jvm.kt b/shared/src/jvmMain/kotlin/dev/serrahima/lluerna/Platform.jvm.kt new file mode 100644 index 0000000..16bf225 --- /dev/null +++ b/shared/src/jvmMain/kotlin/dev/serrahima/lluerna/Platform.jvm.kt @@ -0,0 +1,4 @@ +package dev.serrahima.lluerna + +actual fun platformName(): String = + "desktop (${System.getProperty("os.name")} ${System.getProperty("os.version")})" |
