summaryrefslogtreecommitdiff
path: root/shared/src/commonMain
diff options
context:
space:
mode:
authorGuillermo Serrahima <dev@vylion.com>2026-10-15 02:43:39 +0200
committerGuillermo Serrahima <dev@vylion.com>2026-10-15 02:43:39 +0200
commit785492400072f8f5f10d67ad295befadb98f7baf (patch)
treea3d55ff08a1483ee91d634d06cd4fee874b978ae /shared/src/commonMain
downloadlluerna-main.tar.xz
lluerna-main.zip
initial commitHEADmain
Diffstat (limited to 'shared/src/commonMain')
-rw-r--r--shared/src/commonMain/kotlin/dev/serrahima/lluerna/App.kt36
-rw-r--r--shared/src/commonMain/kotlin/dev/serrahima/lluerna/Platform.kt7
-rw-r--r--shared/src/commonMain/kotlin/dev/serrahima/lluerna/domain/Model.kt74
-rw-r--r--shared/src/commonMain/kotlin/dev/serrahima/lluerna/source/PostSource.kt20
-rw-r--r--shared/src/commonMain/kotlin/dev/serrahima/lluerna/ui/LluernaTheme.kt13
5 files changed, 150 insertions, 0 deletions
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,
+ )
+}