diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 48504c25..1ce29317 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -3,12 +3,8 @@ on: [pull_request]
name: Check pull request
jobs:
test-pr:
- runs-on: macos-latest
- strategy:
- matrix:
- api-level: [23, 29]
+ runs-on: ubuntu-latest
steps:
-
- name: Check if relevant files have changed
uses: actions/github-script@0.9.0
id: service-changed
@@ -34,15 +30,9 @@ jobs:
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- uses: burrunan/gradle-cache-action@v1
- name: Restore cache
-
- - name: Run instrumentation tests
- if: ${{ steps.service-changed.outputs.result == 'true' }}
- uses: reactivecircus/android-emulator-runner@v2.11.0
+ name: Run unit tests
with:
- api-level: ${{ matrix.api-level }}
- target: default
- script: ./gradlew :app:connectedDebugAndroidTest
+ arguments: testDebug
- name: (Fail-only) upload test report
if: failure()
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 0cf9423a..56aa008e 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -13,6 +13,8 @@
+
+
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..bad1a519
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# compose-lobsters
+
+Android app for read-only access to [lobste.rs](https://lobste.rs) community, built with [Jetpack Compose](https://https://developer.android.com/jetpack/compose).
diff --git a/app/build.gradle b/app/build.gradle
index 76efb824..048198ae 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -4,9 +4,10 @@ plugins {
id 'dagger.hilt.android.plugin'
}
+final def keystorePropertiesFile = rootProject.file("keystore.properties")
android {
defaultConfig {
- applicationId "dev.msfjarvis.todo"
+ applicationId "dev.msfjarvis.lobsters"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -25,6 +26,21 @@ android {
kotlinCompilerVersion "${kotlin_version}"
kotlinCompilerExtensionVersion "${compose_version}"
}
+
+ if (keystorePropertiesFile.exists()) {
+ final def keystoreProperties = new Properties()
+ keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
+
+ signingConfigs {
+ release {
+ keyAlias keystoreProperties['keyAlias']
+ keyPassword keystoreProperties['keyPassword']
+ storeFile rootProject.file(keystoreProperties['storeFile'])
+ storePassword keystoreProperties['storePassword']
+ }
+ }
+ buildTypes.release.signingConfig = signingConfigs.release
+ }
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
@@ -37,7 +53,9 @@ dependencies {
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation(project(":data"))
- implementation 'androidx.core:core-ktx:1.5.0-alpha02'
+ implementation(project(":lobsters-api"))
+ implementation(project(":model"))
+ implementation 'androidx.core:core-ktx:1.5.0-alpha03'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
implementation "androidx.compose.foundation:foundation:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
diff --git a/app/src/androidTest/java/dev/msfjarvis/todo/MainActivityTest.kt b/app/src/androidTest/java/dev/msfjarvis/todo/MainActivityTest.kt
deleted file mode 100644
index 6966327d..00000000
--- a/app/src/androidTest/java/dev/msfjarvis/todo/MainActivityTest.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-package dev.msfjarvis.todo
-
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.ui.test.assertIsDisplayed
-import androidx.ui.test.createComposeRule
-import androidx.ui.test.onNodeWithTag
-import androidx.ui.test.onNodeWithText
-import androidx.ui.test.performClick
-import androidx.ui.test.performTextInput
-import dev.msfjarvis.todo.data.model.TodoItem
-import dev.msfjarvis.todo.ui.TodoTheme
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-
-@Ignore("This shit is absolutely fucked")
-class MainActivityTest {
-
- @get:Rule
- val composeTestRule = createComposeRule()
-
- @Test
- fun item_add_dialog_shows_on_fab_click() {
- composeTestRule.apply {
- setContent {
- TodoTheme {
- val items = arrayListOf()
- TodoApp(
- items,
- items::add,
- items::remove,
- )
- }
- }
- onNodeWithTag("fab").performClick()
- onNodeWithTag("item_dialog").assertIsDisplayed()
- }
- }
-
- @Test
- fun item_addition_adds_new_entry() {
- composeTestRule.apply {
- setContent {
- val items by mutableStateOf(arrayListOf())
- TodoTheme {
- TodoApp(
- items,
- items::add,
- items::remove,
- )
- }
- }
- onNodeWithText("Item 1").assertDoesNotExist()
- onNodeWithTag("fab").performClick()
- onNodeWithTag("item_name").performTextInput("Item 1")
- onNodeWithTag("add_button").performClick()
- onNodeWithText("Item 1").assertIsDisplayed()
- }
- }
-
- @Test
- fun item_addition_with_empty_name_does_not_add_new_entry() {
- composeTestRule.apply {
- setContent {
- val items by mutableStateOf(arrayListOf())
- TodoTheme {
- TodoApp(
- items,
- items::add,
- items::remove,
- )
- }
- }
- onNodeWithText("Item 1").assertDoesNotExist()
- onNodeWithTag("fab").performClick()
- onNodeWithTag("add_button").performClick()
- onNodeWithText("Item 1").assertDoesNotExist()
- }
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a2915cbf..481835ee 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,19 +1,21 @@
+ package="dev.msfjarvis.lobsters">
+
+
+ android:theme="@style/Theme.Lobsters">
+ android:theme="@style/Theme.Lobsters.NoActionBar">
@@ -22,4 +24,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/java/dev/msfjarvis/todo/Application.kt b/app/src/main/java/dev/msfjarvis/lobsters/Application.kt
similarity index 80%
rename from app/src/main/java/dev/msfjarvis/todo/Application.kt
rename to app/src/main/java/dev/msfjarvis/lobsters/Application.kt
index 4ff3cad9..f59583cc 100644
--- a/app/src/main/java/dev/msfjarvis/todo/Application.kt
+++ b/app/src/main/java/dev/msfjarvis/lobsters/Application.kt
@@ -1,4 +1,4 @@
-package dev.msfjarvis.todo
+package dev.msfjarvis.lobsters
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
diff --git a/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt
new file mode 100644
index 00000000..ab159edf
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt
@@ -0,0 +1,81 @@
+package dev.msfjarvis.lobsters
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.material.Scaffold
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Providers
+import androidx.compose.runtime.ambientOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.res.stringResource
+import dagger.hilt.android.AndroidEntryPoint
+import dev.msfjarvis.lobsters.api.LobstersApi
+import dev.msfjarvis.lobsters.model.LobstersPost
+import dev.msfjarvis.lobsters.ui.LobstersItem
+import dev.msfjarvis.lobsters.ui.LobstersTheme
+import dev.msfjarvis.lobsters.urllauncher.UrlLauncher
+import kotlinx.coroutines.launch
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+import javax.inject.Inject
+
+val UrlLauncherAmbient = ambientOf { error("Needs to be provided") }
+
+@AndroidEntryPoint
+class MainActivity : AppCompatActivity() {
+ @Inject lateinit var urlLauncher: UrlLauncher
+ @Inject lateinit var apiClient: LobstersApi
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ Providers(UrlLauncherAmbient provides urlLauncher) {
+ LobstersTheme {
+ val coroutineScope = rememberCoroutineScope()
+ val posts = mutableStateListOf()
+ coroutineScope.launch {
+ apiClient.getHottestPosts().enqueue(object : Callback> {
+ override fun onResponse(
+ call: Call>,
+ response: Response>
+ ) {
+ if (response.isSuccessful) {
+ response.body()?.let { posts.addAll(it) }
+ }
+ }
+
+ override fun onFailure(call: Call>, t: Throwable) {
+ TODO("Not yet implemented")
+ }
+ })
+ }
+ LobstersApp(posts)
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun LobstersApp(
+ items: List,
+) {
+ val urlLauncher = UrlLauncherAmbient.current
+
+ Scaffold(
+ topBar = { TopAppBar({ Text(text = stringResource(R.string.app_name)) }) },
+ bodyContent = {
+ LazyColumnFor(items) { item ->
+ LobstersItem(item) { post ->
+ urlLauncher.launch(post.url)
+ }
+ }
+ }
+ )
+}
diff --git a/app/src/main/java/dev/msfjarvis/todo/compose/utils/DeferredIcon.kt b/app/src/main/java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt
similarity index 96%
rename from app/src/main/java/dev/msfjarvis/todo/compose/utils/DeferredIcon.kt
rename to app/src/main/java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt
index 99b6023a..73c0f126 100644
--- a/app/src/main/java/dev/msfjarvis/todo/compose/utils/DeferredIcon.kt
+++ b/app/src/main/java/dev/msfjarvis/lobsters/compose/utils/DeferredIcon.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package dev.msfjarvis.todo.compose.utils
+package dev.msfjarvis.lobsters.compose.utils
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Icon
diff --git a/app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt b/app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt
new file mode 100644
index 00000000..bbfcf5ac
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt
@@ -0,0 +1,17 @@
+package dev.msfjarvis.lobsters.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dev.msfjarvis.lobsters.api.ApiClient
+import dev.msfjarvis.lobsters.api.LobstersApi
+
+@InstallIn(ActivityComponent::class)
+@Module
+object ApiModule {
+ @Provides
+ fun provideLobstersApi(): LobstersApi {
+ return ApiClient.getClient("https://lobste.rs")
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/todo/di/UrlLauncherModule.kt b/app/src/main/java/dev/msfjarvis/lobsters/di/UrlLauncherModule.kt
similarity index 74%
rename from app/src/main/java/dev/msfjarvis/todo/di/UrlLauncherModule.kt
rename to app/src/main/java/dev/msfjarvis/lobsters/di/UrlLauncherModule.kt
index 806aba97..135a2091 100644
--- a/app/src/main/java/dev/msfjarvis/todo/di/UrlLauncherModule.kt
+++ b/app/src/main/java/dev/msfjarvis/lobsters/di/UrlLauncherModule.kt
@@ -1,4 +1,4 @@
-package dev.msfjarvis.todo.di
+package dev.msfjarvis.lobsters.di
import android.content.Context
import dagger.Module
@@ -6,8 +6,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.qualifiers.ActivityContext
-import dev.msfjarvis.todo.urllauncher.UrlLauncher
-import dev.msfjarvis.todo.urllauncher.UrlLauncherImpl
+import dev.msfjarvis.lobsters.urllauncher.UrlLauncher
+import dev.msfjarvis.lobsters.urllauncher.UrlLauncherImpl
@InstallIn(ActivityComponent::class)
@Module
diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt
new file mode 100644
index 00000000..c24c2cc4
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt
@@ -0,0 +1,95 @@
+package dev.msfjarvis.lobsters.ui
+
+import androidx.compose.foundation.Text
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumnFor
+import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ListItem
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import dev.msfjarvis.lobsters.model.LobstersPost
+import dev.msfjarvis.lobsters.model.Submitter
+
+@Composable
+fun LazyItemScope.LobstersItem(
+ post: LobstersPost,
+ modifier: Modifier = Modifier,
+ onClick: (LobstersPost) -> Unit,
+) {
+ ListItem(
+ modifier = modifier.padding(horizontal = 8.dp)
+ .fillParentMaxWidth()
+ .clickable(onClick = { onClick.invoke(post) }),
+ text = {
+ Column {
+ Text(
+ text = post.title,
+ color = Color(0xFF7395D9),
+ fontWeight = FontWeight.Bold,
+ modifier = Modifier.padding(top = 8.dp)
+ )
+ Row(
+ modifier = Modifier.padding(vertical = 8.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ post.tags.forEach { tag ->
+ Text(
+ text = tag,
+ modifier = Modifier
+ .background(Color(0xFFFFFCD7), RoundedCornerShape(4.dp))
+ .padding(4.dp),
+ color = Color.DarkGray,
+ )
+ }
+ }
+ Text(
+ text = "authored by ${post.submitterUser.username}",
+ )
+ }
+ }
+ )
+}
+
+@Composable
+fun PreviewLobstersItem() {
+ val post = LobstersPost(
+ "zqyydb",
+ "https://lobste.rs/s/zqyydb",
+ "2020-09-21T07:11:14.000-05:00",
+ "k2k20 hackathon report: Bob Beck on LibreSSL progress",
+ "https://undeadly.org/cgi?action=article;sid=20200921105847",
+ 4,
+ 0,
+ 0,
+ "",
+ "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on",
+ Submitter(
+ "Vigdis",
+ "2017-02-27T21:08:14.000-06:00",
+ false,
+ "Alleycat for the fun, sys/net admin for a living and OpenBSD contributions for the pleasure. (Not so) French dude in Montreal\r\n\r\nhttps://chown.me",
+ false,
+ 76,
+ "/avatars/Vigdis-100.png",
+ "sevan",
+ null,
+ null,
+ null,
+ ),
+ listOf("openbsd")
+ )
+ LobstersTheme {
+ LazyColumnFor(items = listOf(post)) { item ->
+ LobstersItem(post = item, onClick = {})
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt b/app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt
new file mode 100644
index 00000000..5db0b20f
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt
@@ -0,0 +1,25 @@
+package dev.msfjarvis.lobsters.ui
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.lightColors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+
+val lightColors = lightColors(
+ primary = Color.White,
+ secondary = Color(0xFF6C0000),
+ background = Color.White,
+ surface = Color.White,
+ onPrimary = Color.DarkGray,
+ onSecondary = Color.White,
+ onBackground = Color.Black,
+ onSurface = Color.Black,
+)
+
+@Composable
+fun LobstersTheme(children: @Composable () -> Unit) {
+ MaterialTheme(
+ colors = lightColors,
+ content = children,
+ )
+}
diff --git a/app/src/main/java/dev/msfjarvis/todo/urllauncher/UrlLauncher.kt b/app/src/main/java/dev/msfjarvis/lobsters/urllauncher/UrlLauncher.kt
similarity index 55%
rename from app/src/main/java/dev/msfjarvis/todo/urllauncher/UrlLauncher.kt
rename to app/src/main/java/dev/msfjarvis/lobsters/urllauncher/UrlLauncher.kt
index 16002181..5fc0c4e8 100644
--- a/app/src/main/java/dev/msfjarvis/todo/urllauncher/UrlLauncher.kt
+++ b/app/src/main/java/dev/msfjarvis/lobsters/urllauncher/UrlLauncher.kt
@@ -1,4 +1,4 @@
-package dev.msfjarvis.todo.urllauncher
+package dev.msfjarvis.lobsters.urllauncher
interface UrlLauncher {
fun launch(url: String)
diff --git a/app/src/main/java/dev/msfjarvis/todo/urllauncher/UrlLauncherImpl.kt b/app/src/main/java/dev/msfjarvis/lobsters/urllauncher/UrlLauncherImpl.kt
similarity index 90%
rename from app/src/main/java/dev/msfjarvis/todo/urllauncher/UrlLauncherImpl.kt
rename to app/src/main/java/dev/msfjarvis/lobsters/urllauncher/UrlLauncherImpl.kt
index 67c5b98c..b37e29cd 100644
--- a/app/src/main/java/dev/msfjarvis/todo/urllauncher/UrlLauncherImpl.kt
+++ b/app/src/main/java/dev/msfjarvis/lobsters/urllauncher/UrlLauncherImpl.kt
@@ -1,4 +1,4 @@
-package dev.msfjarvis.todo.urllauncher
+package dev.msfjarvis.lobsters.urllauncher
import android.content.Context
import android.content.Intent
diff --git a/app/src/main/java/dev/msfjarvis/todo/MainActivity.kt b/app/src/main/java/dev/msfjarvis/todo/MainActivity.kt
deleted file mode 100644
index ed4447e9..00000000
--- a/app/src/main/java/dev/msfjarvis/todo/MainActivity.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-package dev.msfjarvis.todo
-
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.Text
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material.AlertDialog
-import androidx.compose.material.Button
-import androidx.compose.material.FloatingActionButton
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.OutlinedTextField
-import androidx.compose.material.Scaffold
-import androidx.compose.material.TopAppBar
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.MutableState
-import androidx.compose.runtime.Providers
-import androidx.compose.runtime.ambientOf
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.setContent
-import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.dp
-import androidx.ui.tooling.preview.Preview
-import dagger.hilt.android.AndroidEntryPoint
-import dev.msfjarvis.todo.compose.utils.IconResource
-import dev.msfjarvis.todo.data.model.TodoItem
-import dev.msfjarvis.todo.data.source.TodoDatabase
-import dev.msfjarvis.todo.ui.ListContent
-import dev.msfjarvis.todo.ui.TodoTheme
-import dev.msfjarvis.todo.urllauncher.UrlLauncher
-import kotlinx.coroutines.launch
-import javax.inject.Inject
-
-val UrlLauncherAmbient = ambientOf { error("Needs to be provided") }
-
-@AndroidEntryPoint
-class MainActivity : AppCompatActivity() {
- @Inject lateinit var database: TodoDatabase
- @Inject lateinit var urlLauncher: UrlLauncher
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContent {
- Providers(UrlLauncherAmbient provides urlLauncher) {
- TodoTheme {
- val coroutineScope = rememberCoroutineScope()
- val itemsDao = database.todoItemsDao()
- val items by itemsDao.getAllItems().collectAsState(initial = emptyList())
- TodoApp(
- items,
- { item -> coroutineScope.launch { itemsDao.insert(item) } },
- { item -> coroutineScope.launch { itemsDao.delete(item) } },
- )
- }
- }
- }
- }
-}
-
-@Composable
-fun TodoApp(
- items: List,
- onAdd: (item: TodoItem) -> Unit,
- onDelete: (item: TodoItem) -> Unit,
-) {
- val showingDialog = remember { mutableStateOf(false) }
- val urlLauncher = UrlLauncherAmbient.current
-
- if (showingDialog.value) {
- ItemAddDialog(
- showingDialog = showingDialog,
- onAdd = onAdd,
- modifier = Modifier.testTag("item_dialog")
- )
- }
-
- Scaffold(
- topBar = { TopAppBar({ Text(text = stringResource(R.string.app_name)) }) },
- floatingActionButton = {
- FloatingActionButton(
- onClick = { showingDialog.value = true },
- elevation = 8.dp,
- modifier = Modifier.testTag("fab")
- ) {
- IconResource(
- resourceId = R.drawable.ic_exposure_plus_1_24dp,
- tint = MaterialTheme.colors.onSecondary,
- )
- }
- },
- bodyContent = {
- ListContent(
- innerPadding = PaddingValues(start = 8.dp, end = 8.dp),
- items = items,
- onSwipe = onDelete::invoke,
- onClick = { urlLauncher.launch(it.title) },
- modifier = Modifier.padding(top = 16.dp),
- )
- },
- )
-}
-
-@Composable
-fun ItemAddDialog(
- showingDialog: MutableState,
- onAdd: (item: TodoItem) -> Unit,
- modifier: Modifier = Modifier,
-) {
- var newItemName by mutableStateOf(TextFieldValue(""))
- val hideDialog = { showingDialog.value = false }
- AlertDialog(
- onDismissRequest = hideDialog,
- text = {
- OutlinedTextField(
- activeColor = MaterialTheme.colors.secondary,
- value = newItemName,
- onValueChange = { newItemName = it },
- label = { Text(text = "Name") },
- modifier = Modifier.testTag("item_name")
- )
- },
- confirmButton = {
- Button(
- onClick = {
- if (newItemName.text.isNotEmpty()) {
- onAdd.invoke(TodoItem(newItemName.text))
- newItemName = TextFieldValue("")
- hideDialog.invoke()
- }
- },
- modifier = Modifier.testTag("add_button")
- ) {
- Text(text = "Add")
- }
- },
- modifier = Modifier then modifier,
- )
-}
-
-@Preview
-@Composable
-fun PreviewApp() {
- TodoTheme {
- val items = arrayListOf(TodoItem("Item 1"))
- TodoApp(
- items,
- items::add,
- items::remove,
- )
- }
-}
diff --git a/app/src/main/java/dev/msfjarvis/todo/di/PersistenceModule.kt b/app/src/main/java/dev/msfjarvis/todo/di/PersistenceModule.kt
deleted file mode 100644
index 28974b7b..00000000
--- a/app/src/main/java/dev/msfjarvis/todo/di/PersistenceModule.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package dev.msfjarvis.todo.di
-
-import android.content.Context
-import androidx.room.Room
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.android.qualifiers.ApplicationContext
-import dagger.hilt.components.SingletonComponent
-import dev.msfjarvis.todo.data.source.TodoDatabase
-
-@InstallIn(SingletonComponent::class)
-@Module
-object PersistenceModule {
- @Provides
- fun provideItemsDatabase(@ApplicationContext context: Context): TodoDatabase {
- return Room.databaseBuilder(context, TodoDatabase::class.java, "data.db")
- .fallbackToDestructiveMigration().build()
- }
-}
diff --git a/app/src/main/java/dev/msfjarvis/todo/ui/AnimatedSwipeDismiss.kt b/app/src/main/java/dev/msfjarvis/todo/ui/AnimatedSwipeDismiss.kt
deleted file mode 100644
index ee3c775c..00000000
--- a/app/src/main/java/dev/msfjarvis/todo/ui/AnimatedSwipeDismiss.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package dev.msfjarvis.todo.ui
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.shrinkVertically
-import androidx.compose.material.DismissDirection
-import androidx.compose.material.DismissValue
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.SwipeToDismiss
-import androidx.compose.material.rememberDismissState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.onCommit
-import androidx.compose.ui.Modifier
-
-@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
-@Composable
-fun AnimatedSwipeDismiss(
- modifier: Modifier = Modifier,
- item: T,
- background: @Composable (isDismissed: Boolean) -> Unit,
- content: @Composable (isDismissed: Boolean) -> Unit,
- directions: Set = setOf(DismissDirection.EndToStart),
- enter: EnterTransition = expandVertically(),
- exit: ExitTransition = shrinkVertically(
- animSpec = tween(
- durationMillis = 500,
- )
- ),
- onDismiss: (T) -> Unit
-) {
- val dismissState = rememberDismissState()
- val isDismissed = dismissState.isDismissed(DismissDirection.EndToStart)
-
- onCommit(dismissState.value) {
- if (dismissState.value == DismissValue.DismissedToStart) {
- onDismiss(item)
- }
- }
-
- AnimatedVisibility(
- modifier = modifier,
- visible = !isDismissed,
- enter = enter,
- exit = exit
- ) {
- SwipeToDismiss(
- modifier = modifier,
- state = dismissState,
- directions = directions,
- background = { background(isDismissed) },
- dismissContent = { content(isDismissed) }
- )
- }
-}
diff --git a/app/src/main/java/dev/msfjarvis/todo/ui/ListContent.kt b/app/src/main/java/dev/msfjarvis/todo/ui/ListContent.kt
deleted file mode 100644
index a0c6a70e..00000000
--- a/app/src/main/java/dev/msfjarvis/todo/ui/ListContent.kt
+++ /dev/null
@@ -1,53 +0,0 @@
-package dev.msfjarvis.todo.ui
-
-import androidx.compose.animation.animate
-import androidx.compose.foundation.Box
-import androidx.compose.foundation.ContentGravity
-import androidx.compose.foundation.Icon
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumnFor
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.DismissDirection
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Delete
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import dev.msfjarvis.todo.data.model.TodoItem
-
-@Composable
-fun ListContent(
- innerPadding: PaddingValues,
- items: List,
- onSwipe: (TodoItem) -> Unit,
- onClick: (TodoItem) -> Unit,
- modifier: Modifier = Modifier,
-) {
- LazyColumnFor(
- modifier = modifier.padding(innerPadding),
- items = items,
- ) { item ->
- AnimatedSwipeDismiss(
- item = item,
- background = { isDismissed ->
- Box(
- modifier = Modifier.background(shape = RoundedCornerShape(8.dp), color = Color.Red)
- .fillMaxSize(),
- paddingStart = 20.dp,
- paddingEnd = 20.dp,
- gravity = ContentGravity.CenterEnd
- ) {
- val alpha = animate(if (isDismissed) 0f else 1f)
- Icon(Icons.Filled.Delete, tint = Color.White.copy(alpha = alpha))
- }
- },
- content = { TodoRowItem(item, onClick) },
- onDismiss = { onSwipe(it) },
- directions = setOf(DismissDirection.EndToStart, DismissDirection.StartToEnd),
- )
- }
-}
diff --git a/app/src/main/java/dev/msfjarvis/todo/ui/TodoRow.kt b/app/src/main/java/dev/msfjarvis/todo/ui/TodoRow.kt
deleted file mode 100644
index 3a85aa1d..00000000
--- a/app/src/main/java/dev/msfjarvis/todo/ui/TodoRow.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package dev.msfjarvis.todo.ui
-
-import androidx.compose.foundation.Text
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyItemScope
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Card
-import androidx.compose.material.ListItem
-import androidx.compose.material.ripple.RippleIndication
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import dev.msfjarvis.todo.data.model.TodoItem
-
-@Composable
-fun LazyItemScope.TodoRowItem(
- item: TodoItem,
- onClick: (TodoItem) -> Unit,
-) {
- Card(
- shape = RoundedCornerShape(8.dp),
- modifier = Modifier.fillParentMaxWidth()
- .clickable(
- onClick = { onClick.invoke(item) },
- indication = RippleIndication()
- ),
- ) {
- ListItem(
- modifier = Modifier.padding(vertical = 8.dp)
- .fillParentMaxWidth(),
- text = {
- Text(
- text = item.title,
- style = TextStyle(
- fontSize = 20.sp,
- textAlign = TextAlign.Center
- ),
- modifier = Modifier.padding(16.dp),
- )
- },
- )
- }
-}
diff --git a/app/src/main/java/dev/msfjarvis/todo/ui/TodoTheme.kt b/app/src/main/java/dev/msfjarvis/todo/ui/TodoTheme.kt
deleted file mode 100644
index c31dc5a7..00000000
--- a/app/src/main/java/dev/msfjarvis/todo/ui/TodoTheme.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package dev.msfjarvis.todo.ui
-
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.graphics.Color
-
-val lightColors = lightColors(
- primary = Color.White,
- secondary = Color(0xFF3700B3),
- background = Color.White,
- surface = Color.White,
- onPrimary = Color.Black,
- onSecondary = Color.White,
- onBackground = Color.Black,
- onSurface = Color.Black,
-)
-val darkColors = darkColors(
- primary = Color(0xFF121212),
- secondary = Color(0xFFBB86FC),
- background = Color.Black,
- surface = Color(0xFF121212),
- onPrimary = Color.White,
- onSecondary = Color.White,
- onBackground = Color.White,
- onSurface = Color.White,
-)
-
-@Composable
-fun TodoTheme(children: @Composable () -> Unit) {
- MaterialTheme(
- colors = if (isSystemInDarkTheme()) darkColors else lightColors,
- content = children,
- )
-}
diff --git a/app/src/main/java/dev/msfjarvis/todo/ui/WireGuardItem.kt b/app/src/main/java/dev/msfjarvis/todo/ui/WireGuardItem.kt
deleted file mode 100644
index 38831de0..00000000
--- a/app/src/main/java/dev/msfjarvis/todo/ui/WireGuardItem.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-package dev.msfjarvis.todo.ui
-
-import androidx.compose.foundation.Text
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyItemScope
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Card
-import androidx.compose.material.ListItem
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Switch
-import androidx.compose.material.ripple.RippleIndication
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import dev.msfjarvis.todo.data.model.TodoItem
-
-@Suppress("Unused")
-@Composable
-fun LazyItemScope.WireGuardItem(item: TodoItem) {
- var checked by remember { mutableStateOf(false) }
- Row(
- modifier = Modifier
- .padding(vertical = 8.dp)
- .fillParentMaxWidth(),
- ) {
- Card(
- shape = RoundedCornerShape(8.dp),
- modifier = Modifier
- .clickable(onClick = { checked = !checked }, indication = RippleIndication())
- .fillParentMaxWidth(),
- backgroundColor = MaterialTheme.colors.secondary
- ) {
- ListItem(
- text = {
- Text(
- text = item.title,
- style = TextStyle(
- fontSize = 20.sp,
- textAlign = TextAlign.Left
- ),
- modifier = Modifier.padding(horizontal = 8.dp, vertical = 16.dp),
- color = MaterialTheme.colors.onSecondary
- )
- },
- trailing = {
- Switch(
- checked = checked,
- onCheckedChange = { checked = it },
- color = MaterialTheme.colors.onSecondary,
- )
- },
- )
- }
- }
-}
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 2b068d11..00000000
--- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher.png b/app/src/main/res/drawable/ic_launcher.png
new file mode 100644
index 00000000..8223fa3f
Binary files /dev/null and b/app/src/main/res/drawable/ic_launcher.png differ
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 07d5da9c..00000000
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_launcher_round.png b/app/src/main/res/drawable/ic_launcher_round.png
new file mode 100644
index 00000000..d5c5e2d0
Binary files /dev/null and b/app/src/main/res/drawable/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index eca70cfe..00000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index eca70cfe..00000000
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
deleted file mode 100644
index a571e600..00000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
deleted file mode 100644
index 61da551c..00000000
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
deleted file mode 100644
index c41dd285..00000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
deleted file mode 100644
index db5080a7..00000000
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
deleted file mode 100644
index 6dba46da..00000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
deleted file mode 100644
index da31a871..00000000
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
deleted file mode 100644
index 15ac6817..00000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
deleted file mode 100644
index b216f2d3..00000000
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
deleted file mode 100644
index f25a4197..00000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
deleted file mode 100644
index e96783cc..00000000
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
index f4111761..e7c6378c 100644
--- a/app/src/main/res/values-night/themes.xml
+++ b/app/src/main/res/values-night/themes.xml
@@ -1,6 +1,6 @@
-
-
-
+
-
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 9a04742f..8f992308 100644
--- a/build.gradle
+++ b/build.gradle
@@ -12,7 +12,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath "com.android.tools.build:gradle:4.2.0-alpha11"
+ classpath "com.android.tools.build:gradle:4.2.0-alpha12"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
diff --git a/data/src/main/AndroidManifest.xml b/data/src/main/AndroidManifest.xml
index 6ca79db8..2357d0f8 100644
--- a/data/src/main/AndroidManifest.xml
+++ b/data/src/main/AndroidManifest.xml
@@ -1,5 +1,2 @@
-
-
-
\ No newline at end of file
+
diff --git a/data/src/main/java/dev/msfjarvis/todo/data/source/DateTimeTypeConverters.kt b/data/src/main/java/dev/msfjarvis/lobsters/data/source/DateTimeTypeConverters.kt
similarity index 96%
rename from data/src/main/java/dev/msfjarvis/todo/data/source/DateTimeTypeConverters.kt
rename to data/src/main/java/dev/msfjarvis/lobsters/data/source/DateTimeTypeConverters.kt
index 581573da..2f36e09d 100644
--- a/data/src/main/java/dev/msfjarvis/todo/data/source/DateTimeTypeConverters.kt
+++ b/data/src/main/java/dev/msfjarvis/lobsters/data/source/DateTimeTypeConverters.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package dev.msfjarvis.todo.data.source
+package dev.msfjarvis.lobsters.data.source
import androidx.room.TypeConverter
import java.time.LocalDateTime
diff --git a/data/src/main/java/dev/msfjarvis/todo/data/model/TodoItem.kt b/data/src/main/java/dev/msfjarvis/todo/data/model/TodoItem.kt
deleted file mode 100644
index a34a51a2..00000000
--- a/data/src/main/java/dev/msfjarvis/todo/data/model/TodoItem.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package dev.msfjarvis.todo.data.model
-
-import androidx.room.Entity
-import androidx.room.PrimaryKey
-import java.time.LocalDateTime
-import java.time.ZoneId
-
-@Entity(
- tableName = "todo_items",
-)
-data class TodoItem(
- @PrimaryKey val title: String,
- val time: LocalDateTime = LocalDateTime.now(ZoneId.of("GMT")),
-)
diff --git a/data/src/main/java/dev/msfjarvis/todo/data/source/TodoDatabase.kt b/data/src/main/java/dev/msfjarvis/todo/data/source/TodoDatabase.kt
deleted file mode 100644
index e971a957..00000000
--- a/data/src/main/java/dev/msfjarvis/todo/data/source/TodoDatabase.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package dev.msfjarvis.todo.data.source
-
-import androidx.room.Database
-import androidx.room.RoomDatabase
-import androidx.room.TypeConverters
-import dev.msfjarvis.todo.data.model.TodoItem
-
-@Database(
- entities = [
- TodoItem::class,
- ],
- version = 1,
- exportSchema = false,
-)
-@TypeConverters(DateTimeTypeConverters::class)
-abstract class TodoDatabase : RoomDatabase() {
- abstract fun todoItemsDao(): TodoItemDao
-}
diff --git a/data/src/main/java/dev/msfjarvis/todo/data/source/TodoItemDao.kt b/data/src/main/java/dev/msfjarvis/todo/data/source/TodoItemDao.kt
deleted file mode 100644
index 7454145f..00000000
--- a/data/src/main/java/dev/msfjarvis/todo/data/source/TodoItemDao.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-package dev.msfjarvis.todo.data.source
-
-import androidx.room.Dao
-import androidx.room.Delete
-import androidx.room.Insert
-import androidx.room.OnConflictStrategy
-import androidx.room.Query
-import androidx.room.Update
-import dev.msfjarvis.todo.data.model.TodoItem
-import kotlinx.coroutines.flow.Flow
-
-/**
- * Room [Dao] for [TodoItem]
- */
-@Dao
-abstract class TodoItemDao {
-
- @Query("SELECT * FROM todo_items")
- abstract fun getAllItems(): Flow>
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- abstract suspend fun insert(entity: TodoItem): Long
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- abstract suspend fun insertAll(vararg entity: TodoItem)
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- abstract suspend fun insertAll(entities: Collection)
-
- @Update(onConflict = OnConflictStrategy.REPLACE)
- abstract suspend fun update(entity: TodoItem)
-
- @Delete
- abstract suspend fun delete(entity: TodoItem): Int
-}
diff --git a/lobsters-api/.gitignore b/lobsters-api/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/lobsters-api/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/lobsters-api/build.gradle b/lobsters-api/build.gradle
new file mode 100644
index 00000000..7c1d953e
--- /dev/null
+++ b/lobsters-api/build.gradle
@@ -0,0 +1,17 @@
+plugins {
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+}
+
+dependencies {
+ def moshi_version = "1.9.3"
+ def retrofit_version = "2.9.0"
+ implementation project(":model")
+ api "com.squareup.retrofit2:retrofit:$retrofit_version"
+ implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version"
+ kaptTest "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
+ testImplementation 'junit:junit:4.13'
+ // retrofit uses 3.14.9, so shall we.
+ //noinspection GradleDependency
+ testImplementation "com.squareup.okhttp3:mockwebserver:3.14.9"
+}
diff --git a/lobsters-api/src/main/AndroidManifest.xml b/lobsters-api/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..fce8aad8
--- /dev/null
+++ b/lobsters-api/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/lobsters-api/src/main/java/dev/msfjarvis/lobsters/api/ApiClient.kt b/lobsters-api/src/main/java/dev/msfjarvis/lobsters/api/ApiClient.kt
new file mode 100644
index 00000000..26f72f56
--- /dev/null
+++ b/lobsters-api/src/main/java/dev/msfjarvis/lobsters/api/ApiClient.kt
@@ -0,0 +1,14 @@
+package dev.msfjarvis.lobsters.api
+
+import retrofit2.Retrofit
+import retrofit2.converter.moshi.MoshiConverterFactory
+
+object ApiClient {
+ inline fun getClient(baseUrl: String): T {
+ return Retrofit.Builder()
+ .baseUrl(baseUrl)
+ .addConverterFactory(MoshiConverterFactory.create())
+ .build()
+ .create(T::class.java)
+ }
+}
diff --git a/lobsters-api/src/main/java/dev/msfjarvis/lobsters/api/LobstersApi.kt b/lobsters-api/src/main/java/dev/msfjarvis/lobsters/api/LobstersApi.kt
new file mode 100644
index 00000000..5422bc7a
--- /dev/null
+++ b/lobsters-api/src/main/java/dev/msfjarvis/lobsters/api/LobstersApi.kt
@@ -0,0 +1,10 @@
+package dev.msfjarvis.lobsters.api
+
+import dev.msfjarvis.lobsters.model.LobstersPost
+import retrofit2.Call
+import retrofit2.http.GET
+
+interface LobstersApi {
+ @GET("hottest.json")
+ fun getHottestPosts(): Call>
+}
diff --git a/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/LobstersApiTest.kt b/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/LobstersApiTest.kt
new file mode 100644
index 00000000..2e0305b7
--- /dev/null
+++ b/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/LobstersApiTest.kt
@@ -0,0 +1,54 @@
+package dev.msfjarvis.lobsters.api
+
+import dev.msfjarvis.lobsters.model.LobstersPost
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.fail
+import org.junit.Before
+import org.junit.Test
+import retrofit2.Call
+import retrofit2.Callback
+import retrofit2.Response
+
+class LobstersApiTest {
+ private val webServer = MockWebServer()
+ private val apiData = TestUtils.getJson("hottest.json")
+ private val apiClient = ApiClient.getClient("http://localhost:8080")
+
+ @Before
+ fun setUp() {
+ webServer.start(8080)
+ webServer.dispatcher = object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse {
+ return MockResponse().setBody(apiData).setResponseCode(200)
+ }
+ }
+ }
+
+ @Test
+ fun `api gets correct number of items`() {
+ apiClient.getHottestPosts().enqueue(object : Callback> {
+ override fun onResponse(
+ call: Call>,
+ response: Response>
+ ) {
+ val posts = response.body()
+ require(posts != null)
+ assertEquals(25, posts.size)
+ }
+
+ override fun onFailure(call: Call>, t: Throwable) {
+ fail("Call cannot fail in tests")
+ }
+ })
+ }
+
+ @After
+ fun tearDown() {
+ webServer.shutdown()
+ }
+}
diff --git a/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt b/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt
new file mode 100644
index 00000000..04e358b7
--- /dev/null
+++ b/lobsters-api/src/test/java/dev/msfjarvis/lobsters/api/TestUtils.kt
@@ -0,0 +1,12 @@
+package dev.msfjarvis.lobsters.api
+
+import java.io.File
+
+object TestUtils {
+ fun getJson(path : String) : String {
+ // Load the JSON response
+ val uri = javaClass.classLoader.getResource(path)
+ val file = File(uri.path)
+ return String(file.readBytes())
+ }
+}
diff --git a/lobsters-api/src/test/resources/hottest.json b/lobsters-api/src/test/resources/hottest.json
new file mode 100644
index 00000000..4e3f86a6
--- /dev/null
+++ b/lobsters-api/src/test/resources/hottest.json
@@ -0,0 +1 @@
+[{"short_id":"q1hh1g","short_id_url":"https://lobste.rs/s/q1hh1g","created_at":"2020-09-21T08:04:24.000-05:00","title":"Simple Anomaly Detection Using Plain SQL","url":"https://hakibenita.com/sql-anomaly-detection","score":33,"flags":0,"comment_count":5,"description":"","comments_url":"https://lobste.rs/s/q1hh1g/simple_anomaly_detection_using_plain_sql","submitter_user":{"username":"Haki","created_at":"2019-01-04T01:25:42.000-06:00","is_admin":false,"about":"https://hakibenita.com","is_moderator":false,"karma":278,"avatar_url":"/avatars/Haki-100.png","invited_by_user":"pstef"},"tags":["databases"]},{"short_id":"gnd8bc","short_id_url":"https://lobste.rs/s/gnd8bc","created_at":"2020-09-21T11:56:04.000-05:00","title":"scalar: A small chat protocol, inspired by Gemini","url":"https://sr.ht/~icefox/scalar/","score":18,"flags":0,"comment_count":4,"description":"","comments_url":"https://lobste.rs/s/gnd8bc/scalar_small_chat_protocol_inspired_by","submitter_user":{"username":"icefox","created_at":"2018-08-26T20:59:16.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":2818,"avatar_url":"/avatars/icefox-100.png","invited_by_user":"shanemhansen"},"tags":["networking","release","show"]},{"short_id":"bssphv","short_id_url":"https://lobste.rs/s/bssphv","created_at":"2020-09-21T14:21:46.000-05:00","title":"My Least Favorite Rust Type","url":"https://ridiculousfish.com/blog/posts/least-favorite-rust-type.html","score":23,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/bssphv/my_least_favorite_rust_type","submitter_user":{"username":"liftM","created_at":"2017-11-18T04:29:06.000-06:00","is_admin":false,"about":"I work on programming languages and build systems.","is_moderator":false,"karma":161,"avatar_url":"/avatars/liftM-100.png","invited_by_user":"alok","github_username":"liftM","twitter_username":"liftm2"},"tags":["rust"]},{"short_id":"d8wxhi","short_id_url":"https://lobste.rs/s/d8wxhi","created_at":"2020-09-20T20:34:16.000-05:00","title":"On the use of a life","url":"http://www.daemonology.net/blog/2020-09-20-On-the-use-of-a-life.html","score":103,"flags":5,"comment_count":12,"description":"","comments_url":"https://lobste.rs/s/d8wxhi/on_use_life","submitter_user":{"username":"amontalenti","created_at":"2014-02-11T09:12:37.000-06:00","is_admin":false,"about":"Founder of [Parse.ly](http://parse.ly). Python, Clojure, JavaScript, \u0026 C. UNIX lover. Web hacker. Blogging at [amontalenti.com](https://amontalenti.com), tweeting at [@amontalenti](http://twitter.com/amontalenti).","is_moderator":false,"karma":930,"avatar_url":"/avatars/amontalenti-100.png","invited_by_user":"conroy"},"tags":["crypto","freebsd","person"]},{"short_id":"gxgoel","short_id_url":"https://lobste.rs/s/gxgoel","created_at":"2020-09-21T13:22:08.000-05:00","title":"Plan 9 rides again; WSL file access","url":"https://nelsonslog.wordpress.com/2019/02/16/plan-9-rides-again-wsl-file-access/","score":13,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/gxgoel/plan_9_rides_again_wsl_file_access","submitter_user":{"username":"awreece","created_at":"2016-01-15T00:04:45.000-06:00","is_admin":false,"about":"My hobbies are systems performance, computer security, and weird bugs. http://codearcana.com/","is_moderator":false,"karma":286,"avatar_url":"/avatars/awreece-100.png","invited_by_user":"peter"},"tags":["linux","osdev","windows"]},{"short_id":"hvr16d","short_id_url":"https://lobste.rs/s/hvr16d","created_at":"2020-09-21T05:17:04.000-05:00","title":"What are you doing this week?","url":"","score":16,"flags":0,"comment_count":20,"description":"\u003cp\u003eWhat are you doing this week? Feel free to share!\u003c/p\u003e\n\u003cp\u003eKeep in mind it’s OK to do nothing at all, too.\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/hvr16d/what_are_you_doing_this_week","submitter_user":{"username":"caius","created_at":"2014-05-13T06:58:30.000-05:00","is_admin":false,"about":"Compulsive Geek, Ale Connoisseur, Head of Engineering at [SafeguardingMonitor](https://safeguardingmonitor.co.uk), Occasionally Responsible Adult.","is_moderator":false,"karma":5077,"avatar_url":"/avatars/caius-100.png","invited_by_user":"lauris","github_username":"caius","twitter_username":"Caius"},"tags":["ask","programming"]},{"short_id":"jgcvev","short_id_url":"https://lobste.rs/s/jgcvev","created_at":"2020-09-20T15:25:25.000-05:00","title":"Why Not Rust?","url":"https://matklad.github.io//2020/09/20/why-not-rust.html","score":60,"flags":0,"comment_count":25,"description":"","comments_url":"https://lobste.rs/s/jgcvev/why_not_rust","submitter_user":{"username":"notriddle","created_at":"2018-08-30T09:08:52.000-05:00","is_admin":false,"about":"https://notriddle.com/","is_moderator":false,"karma":2864,"avatar_url":"/avatars/notriddle-100.png","invited_by_user":"zimbatm","github_username":"notriddle"},"tags":["rust"]},{"short_id":"xhrskw","short_id_url":"https://lobste.rs/s/xhrskw","created_at":"2020-09-21T05:42:07.000-05:00","title":"Is it possible to hide all \"What are you doing this XXX\" threads?","url":"","score":33,"flags":0,"comment_count":21,"description":"\u003cp\u003eI’m annoyed by all these “What are you doing this XXX” threads.\nIs it possible to automatically hide them?\u003c/p\u003e\n\u003cp\u003eThey are usually tagged with “ask” and “programming”.\nSo hiding all “ask” and “programming” threads in my settings\nprobably would do the trick,\nbut then I’d also miss the interesting questions…\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/xhrskw/is_it_possible_hide_all_what_are_you_doing","submitter_user":{"username":"hwj","created_at":"2019-05-30T00:12:36.000-05:00","is_admin":false,"about":"computer science student","is_moderator":false,"karma":763,"avatar_url":"/avatars/hwj-100.png","invited_by_user":"nickpsecurity"},"tags":["meta"]},{"short_id":"xuvwrb","short_id_url":"https://lobste.rs/s/xuvwrb","created_at":"2020-09-21T10:22:37.000-05:00","title":"The unrealized potential of federation","url":"https://drewdevault.com/2020/09/20/The-potential-of-federation.html","score":7,"flags":1,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/xuvwrb/unrealized_potential_federation","submitter_user":{"username":"zge","created_at":"2017-11-19T11:49:35.000-06:00","is_admin":false,"about":"CS student and hobby developer.","is_moderator":false,"karma":9038,"avatar_url":"/avatars/zge-100.png","invited_by_user":"josuah","github_username":"phikal"},"tags":["networking"]},{"short_id":"9t1ves","short_id_url":"https://lobste.rs/s/9t1ves","created_at":"2020-09-21T06:53:28.000-05:00","title":"Creating a Home IPv6 Network","url":"https://blog.hansenpartnership.com/creating-a-home-ipv6-network/","score":14,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/9t1ves/creating_home_ipv6_network","submitter_user":{"username":"freddyb","created_at":"2017-02-02T09:12:16.000-06:00","is_admin":false,"about":"Security. Mostly in Browsers, but not exclusively.","is_moderator":false,"karma":3873,"avatar_url":"/avatars/freddyb-100.png","invited_by_user":"stas","keybase_signatures":[{"kb_username":"freddyb","sig_hash":"550e2f5b27d4b5d558c02dfb2b23a628e90635183c93f993eeea4e16b20c51150f"}]},"tags":["linux","networking"]},{"short_id":"8m7ydc","short_id_url":"https://lobste.rs/s/8m7ydc","created_at":"2020-09-21T11:18:06.000-05:00","title":"dstask: Single binary terminal-based TODO manager with git-based sync + markdown notes per task","url":"https://github.com/naggie/dstask","score":5,"flags":0,"comment_count":4,"description":"\u003cp\u003eShowcasing on behalf of the author\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/8m7ydc/dstask_single_binary_terminal_based_todo","submitter_user":{"username":"JordiGH","created_at":"2014-11-24T16:21:05.000-06:00","is_admin":false,"about":"Jordi Gutiérrez Hermoso. \r\n\r\nGNU Octave dev, Mercurial enthusiast.\r\n\r\nCoder, mathematician, hacker-errant.\r\n\r\nYou may contact me at jordigh@octave.org","is_moderator":false,"karma":6718,"avatar_url":"/avatars/JordiGH-100.png","invited_by_user":"technomancy"},"tags":["go","show"]},{"short_id":"lzaycw","short_id_url":"https://lobste.rs/s/lzaycw","created_at":"2020-09-21T13:01:05.000-05:00","title":"Maybe don’t write off Scala just yet","url":"https://levelup.gitconnected.com/maybe-dont-write-off-scala-just-yet-f0c128a570f0","score":5,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/lzaycw/maybe_don_t_write_off_scala_just_yet","submitter_user":{"username":"asteroid","created_at":"2020-01-29T13:58:14.000-06:00","is_admin":false,"about":"Writer. Editor. Computer geek. Chocoholic. Baseball fan. Not always in that order.","is_moderator":false,"karma":333,"avatar_url":"/avatars/asteroid-100.png","invited_by_user":"petdance","twitter_username":"estherschindler"},"tags":["scala"]},{"short_id":"uqiz1y","short_id_url":"https://lobste.rs/s/uqiz1y","created_at":"2020-09-21T16:37:37.000-05:00","title":"Croquet Project Demo (2003)","url":"https://www.youtube.com/watch?v=cXGLOiZUZ2U","score":3,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/uqiz1y/croquet_project_demo_2003","submitter_user":{"username":"sevan","created_at":"2013-06-02T17:42:02.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":6732,"avatar_url":"/avatars/sevan-100.png","invited_by_user":"jturner","github_username":"sevan"},"tags":["graphics","video"]},{"short_id":"9tdnvp","short_id_url":"https://lobste.rs/s/9tdnvp","created_at":"2020-09-20T12:40:46.000-05:00","title":"uMatrix development has ended","url":"https://www.ghacks.net/2020/09/20/umatrix-development-has-ended/","score":38,"flags":1,"comment_count":13,"description":"","comments_url":"https://lobste.rs/s/9tdnvp/umatrix_development_has_ended","submitter_user":{"username":"skrzyp","created_at":"2016-05-21T16:41:57.000-05:00","is_admin":false,"about":"````\r\n````","is_moderator":false,"karma":867,"avatar_url":"/avatars/skrzyp-100.png","invited_by_user":"mulander"},"tags":["browsers","privacy","web"]},{"short_id":"mi4tlk","short_id_url":"https://lobste.rs/s/mi4tlk","created_at":"2020-09-21T08:28:38.000-05:00","title":"Deep Learning in Clojure with Fewer Parentheses than Keras and Python","url":"https://dragan.rocks/articles/20/Deep-Diamond-Deep-Learning-in-Clojure-Fewer-Parentheses-Python-Keras","score":4,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/mi4tlk/deep_learning_clojure_with_fewer","submitter_user":{"username":"dragandj","created_at":"2016-05-12T13:12:22.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":154,"avatar_url":"/avatars/dragandj-100.png","invited_by_user":"mindcrime"},"tags":["ai","clojure","java","python"]},{"short_id":"ztsooj","short_id_url":"https://lobste.rs/s/ztsooj","created_at":"2020-09-21T06:39:34.000-05:00","title":"14nm and 7nm are NOT what you think it is","url":"https://www.youtube.com/watch?v=1kQUXpZpLXI","score":7,"flags":0,"comment_count":2,"description":"","comments_url":"https://lobste.rs/s/ztsooj/14nm_7nm_are_not_what_you_think_it_is","submitter_user":{"username":"asymptotically","created_at":"2019-08-11T09:22:13.000-05:00","is_admin":false,"about":"","is_moderator":false,"karma":558,"avatar_url":"/avatars/asymptotically-100.png","invited_by_user":"gerikson"},"tags":["hardware","video"]},{"short_id":"wgfc92","short_id_url":"https://lobste.rs/s/wgfc92","created_at":"2020-09-21T13:43:50.000-05:00","title":"EDN parser and generator for TS/JS working with plain data and stream support","url":"https://github.com/jorinvo/edn-data","score":1,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/wgfc92/edn_parser_generator_for_ts_js_working","submitter_user":{"username":"jorin","created_at":"2020-07-06T11:34:49.000-05:00","is_admin":false,"about":"✨ decentralize all the things ✨ #clojure is just data 🖤 \r\n🌱 plant trees 🌳\r\n→ experimenting with feedback systems 🤖\r\nFind me at https://mas.to/@jorin","is_moderator":false,"karma":52,"avatar_url":"/avatars/jorin-100.png","invited_by_user":"jussi","github_username":"jorinvo","twitter_username":"jorinvo"},"tags":["clojure","javascript","nodejs","release"]},{"short_id":"bhttyk","short_id_url":"https://lobste.rs/s/bhttyk","created_at":"2020-09-20T11:28:13.000-05:00","title":"organice (Org mode for mobile devices and the browser) renders clickable links automatically","url":"https://200ok.ch/posts/2020-09-20_organice_renders_clickable_links_automatically.html","score":20,"flags":0,"comment_count":7,"description":"","comments_url":"https://lobste.rs/s/bhttyk/organice_org_mode_for_mobile_devices","submitter_user":{"username":"munen","created_at":"2019-09-23T02:27:28.000-05:00","is_admin":false,"about":"CEO 200ok.ch, Lecturer at ZHAW. Ordained Zen Buddhist monk and caretaker of the Lambda Zen Temple (http://zen-temple.net).","is_moderator":false,"karma":296,"avatar_url":"/avatars/munen-100.png","invited_by_user":"bandali","github_username":"munen"},"tags":["emacs"]},{"short_id":"lfslfx","short_id_url":"https://lobste.rs/s/lfslfx","created_at":"2020-09-21T04:28:22.000-05:00","title":"Demystifying AWS VPC","url":"https://scorpil.com/post/aws-vpc/","score":8,"flags":0,"comment_count":0,"description":"\u003cp\u003eThis is posted on behalf of \u003ca href=\"https://lobste.rs/u/scorpil\" rel=\"ugc\"\u003e@scorpil\u003c/a\u003e, who can’t post their own stuff yet. I found it relevant to this community (but as I’m their inviter I might be biased)\u003c/p\u003e\n","comments_url":"https://lobste.rs/s/lfslfx/demystifying_aws_vpc","submitter_user":{"username":"gerikson","created_at":"2017-01-13T03:16:10.000-06:00","is_admin":false,"about":"Swede, father and husband. \r\n\r\nhttp://gerikson.com/blog/\r\n\r\n### Invitation policy\r\n\r\nNote that this is my personal policy. If I don't extend an invite, it doesn't mean no-one else will. Be polite, be open, and be patient.\r\n\r\nFirst step, please reach out to the community in [chat](https://lobste.rs/chat) and let us know why you want to be a part of the community. \r\n\r\nI will typically request some form of proof that your username is associated with a public profile on a social network (and yes, I count GitHub as a social network). If you do not wish to share such information in the general chat, I will not consider extending an invite. \r\n\r\nI will probably not extend an invite if your online behavior exhibits sexism, misogyny, racism or homophobia. \r\n\r\nI will politely ignore unsolicitated requests for invites via Twitter DM or email.","is_moderator":false,"karma":7097,"avatar_url":"/avatars/gerikson-100.png","invited_by_user":"varjag","github_username":"gustafe","twitter_username":"gerikson"},"tags":["distributed","networking"]},{"short_id":"ote3wb","short_id_url":"https://lobste.rs/s/ote3wb","created_at":"2020-09-21T03:17:38.000-05:00","title":"D Tetris running on Webassembly","url":"http://dpldocs.info/this-week-in-d/Blog.Posted_2020_08_10.html","score":8,"flags":0,"comment_count":1,"description":"","comments_url":"https://lobste.rs/s/ote3wb/d_tetris_running_on_webassembly","submitter_user":{"username":"speps","created_at":"2017-09-06T02:15:19.000-05:00","is_admin":false,"about":"Senior Software Engineer at Rare Ltd (Microsoft Studios UK). All opinions are my own and do not reflect my employer's.","is_moderator":false,"karma":233,"avatar_url":"/avatars/speps-100.png","invited_by_user":"mikejsavage"},"tags":["d","wasm"]},{"short_id":"vkm0ad","short_id_url":"https://lobste.rs/s/vkm0ad","created_at":"2020-09-21T07:59:49.000-05:00","title":"k2k20 hackathon report: Klemens Nanni on network land decluttering","url":"http://undeadly.org/cgi?action=article;sid=20200921110059","score":5,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/vkm0ad/k2k20_hackathon_report_klemens_nanni_on","submitter_user":{"username":"calvin","created_at":"2014-07-01T06:47:13.000-05:00","is_admin":false,"about":"Soon we will all have special names... names designed to make the cathode ray tube resonate.","is_moderator":false,"karma":66870,"avatar_url":"/avatars/calvin-100.png","invited_by_user":"nbyouri","github_username":"NattyNarwhal"},"tags":["openbsd"]},{"short_id":"liy030","short_id_url":"https://lobste.rs/s/liy030","created_at":"2020-09-21T14:21:28.000-05:00","title":"Local memoized recursive functions","url":"https://quanttype.net/posts/2020-09-20-local-memoized-recursive-functions.html","score":2,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/liy030/local_memoized_recursive_functions","submitter_user":{"username":"jussi","created_at":"2016-06-07T11:38:09.000-05:00","is_admin":false,"about":"Programmer at Metosin\r\n\r\nClojure/Python/Javascript/Rust/Ruby/DuckDuckGo.\r\n\r\n","is_moderator":false,"karma":407,"avatar_url":"/avatars/jussi-100.png","invited_by_user":"flyingfisch","github_username":"jrasanen","twitter_username":"jussiras"},"tags":["clojure","programming"]},{"short_id":"lcb5us","short_id_url":"https://lobste.rs/s/lcb5us","created_at":"2020-09-21T08:56:55.000-05:00","title":"Analyzing Python Code with Python","url":"https://rotemtam.com/2020/08/13/python-ast/","score":4,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/lcb5us/analyzing_python_code_with_python","submitter_user":{"username":"learnbyexample","created_at":"2020-06-15T09:51:10.000-05:00","is_admin":false,"about":"Sundeep Agarwal is a freelance trainer, [author](https://learnbyexample.github.io/books/) and mentor. You can find his works, primarily focused on Linux command line, text processing, scripting languages and curated lists, at [https://github.com/learnbyexample](https://github.com/learnbyexample).","is_moderator":false,"karma":347,"avatar_url":"/avatars/learnbyexample-100.png","invited_by_user":"ngoldbaum"},"tags":["python","testing"]},{"short_id":"rmlqmn","short_id_url":"https://lobste.rs/s/rmlqmn","created_at":"2020-09-21T13:19:01.000-05:00","title":"Data art posters about music (streaming) data for Sony Music","url":"https://www.visualcinnamon.com/2020/06/sony-music-data-art","score":2,"flags":1,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/rmlqmn/data_art_posters_about_music_streaming","submitter_user":{"username":"danburzo","created_at":"2018-10-02T10:53:45.000-05:00","is_admin":false,"about":"Building things for the web. Learning in public. Co-founder of [Moqups](https://moqups.com). Find me [on Mastodon](https://mastodon.social/@danburzo).","is_moderator":false,"karma":1159,"avatar_url":"/avatars/danburzo-100.png","invited_by_user":"migurski","github_username":"danburzo","twitter_username":"danburzo"},"tags":["design","visualization"]},{"short_id":"zqyydb","short_id_url":"https://lobste.rs/s/zqyydb","created_at":"2020-09-21T07:11:14.000-05:00","title":"k2k20 hackathon report: Bob Beck on LibreSSL progress","url":"https://undeadly.org/cgi?action=article;sid=20200921105847","score":4,"flags":0,"comment_count":0,"description":"","comments_url":"https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on","submitter_user":{"username":"Vigdis","created_at":"2017-02-27T21:08:14.000-06:00","is_admin":false,"about":"Alleycat for the fun, sys/net admin for a living and OpenBSD contributions for the pleasure. (Not so) French dude in Montreal\r\n\r\nhttps://chown.me","is_moderator":false,"karma":76,"avatar_url":"/avatars/Vigdis-100.png","invited_by_user":"sevan"},"tags":["openbsd"]}]
\ No newline at end of file
diff --git a/model/.gitignore b/model/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/model/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/model/build.gradle b/model/build.gradle
new file mode 100644
index 00000000..51aade2d
--- /dev/null
+++ b/model/build.gradle
@@ -0,0 +1,12 @@
+plugins {
+ id 'kotlin-android'
+ id 'kotlin-kapt'
+}
+
+dependencies {
+ def moshi_version = "1.9.3"
+ api "androidx.room:room-runtime:$room_version"
+ kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
+ implementation "com.squareup.moshi:moshi:$moshi_version"
+ implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"
+}
diff --git a/model/src/main/AndroidManifest.xml b/model/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..48e8717e
--- /dev/null
+++ b/model/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/model/src/main/java/dev/msfjarvis/lobsters/model/KeybaseSignature.kt b/model/src/main/java/dev/msfjarvis/lobsters/model/KeybaseSignature.kt
new file mode 100644
index 00000000..6002a693
--- /dev/null
+++ b/model/src/main/java/dev/msfjarvis/lobsters/model/KeybaseSignature.kt
@@ -0,0 +1,12 @@
+package dev.msfjarvis.lobsters.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+class KeybaseSignature(
+ @Json(name = "kb_username")
+ val kbUsername: String,
+ @Json(name = "sig_hash")
+ val sigHash: String
+)
diff --git a/model/src/main/java/dev/msfjarvis/lobsters/model/LobstersPost.kt b/model/src/main/java/dev/msfjarvis/lobsters/model/LobstersPost.kt
new file mode 100644
index 00000000..db283b94
--- /dev/null
+++ b/model/src/main/java/dev/msfjarvis/lobsters/model/LobstersPost.kt
@@ -0,0 +1,32 @@
+package dev.msfjarvis.lobsters.model
+
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@Entity(
+ tableName = "lobsters_posts"
+)
+@JsonClass(generateAdapter = true)
+class LobstersPost(
+ @Json(name = "short_id")
+ @PrimaryKey
+ val shortId: String,
+ @Json(name = "short_id_url")
+ val shortIdUrl: String,
+ @Json(name = "created_at")
+ val createdAt: String,
+ val title: String,
+ val url: String,
+ val score: Long,
+ val flags: Long,
+ @Json(name = "comment_count")
+ val commentCount: Long,
+ val description: String,
+ @Json(name = "comments_url")
+ val commentsUrl: String,
+ @Json(name = "submitter_user")
+ val submitterUser: Submitter,
+ val tags: List
+)
diff --git a/model/src/main/java/dev/msfjarvis/lobsters/model/Submitter.kt b/model/src/main/java/dev/msfjarvis/lobsters/model/Submitter.kt
new file mode 100644
index 00000000..929bc520
--- /dev/null
+++ b/model/src/main/java/dev/msfjarvis/lobsters/model/Submitter.kt
@@ -0,0 +1,27 @@
+package dev.msfjarvis.lobsters.model
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+class Submitter(
+ val username: String,
+ @Json(name = "created_at")
+ val createdAt: String,
+ @Json(name = "is_admin")
+ val isAdmin: Boolean,
+ val about: String,
+ @Json(name = "is_moderator")
+ val isModerator: Boolean,
+ val karma: Long,
+ @Json(name = "avatar_url")
+ val avatarUrl: String,
+ @Json(name = "invited_by_user")
+ val invitedByUser: String,
+ @Json(name = "github_username")
+ val githubUsername: String?,
+ @Json(name = "twitter_username")
+ val twitterUsername: String?,
+ @Json(name = "keybase_signatures")
+ val keybaseSignatures: List?
+)
diff --git a/settings.gradle b/settings.gradle
index 6f56f806..f1785d43 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1,3 +1,5 @@
-rootProject.name = "Compose-ToDo"
+rootProject.name = "lobste.rs"
include ':app'
include ':data'
+include ':lobsters-api'
+include ':model'