summaryrefslogtreecommitdiff
path: root/shared
diff options
context:
space:
mode:
Diffstat (limited to 'shared')
-rw-r--r--shared/build.gradle.kts46
-rw-r--r--shared/src/androidMain/kotlin/dev/serrahima/lluerna/Platform.android.kt6
-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
-rw-r--r--shared/src/jvmMain/kotlin/dev/serrahima/lluerna/Platform.jvm.kt4
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")})"