Merge pull request #20 from msfjarvis/pivot-to-lobsters
16
.github/workflows/pull_request.yml
vendored
|
@ -3,12 +3,8 @@ on: [pull_request]
|
||||||
name: Check pull request
|
name: Check pull request
|
||||||
jobs:
|
jobs:
|
||||||
test-pr:
|
test-pr:
|
||||||
runs-on: macos-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
api-level: [23, 29]
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Check if relevant files have changed
|
- name: Check if relevant files have changed
|
||||||
uses: actions/github-script@0.9.0
|
uses: actions/github-script@0.9.0
|
||||||
id: service-changed
|
id: service-changed
|
||||||
|
@ -34,15 +30,9 @@ jobs:
|
||||||
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties
|
||||||
|
|
||||||
- uses: burrunan/gradle-cache-action@v1
|
- uses: burrunan/gradle-cache-action@v1
|
||||||
name: Restore cache
|
name: Run unit tests
|
||||||
|
|
||||||
- name: Run instrumentation tests
|
|
||||||
if: ${{ steps.service-changed.outputs.result == 'true' }}
|
|
||||||
uses: reactivecircus/android-emulator-runner@v2.11.0
|
|
||||||
with:
|
with:
|
||||||
api-level: ${{ matrix.api-level }}
|
arguments: testDebug
|
||||||
target: default
|
|
||||||
script: ./gradlew :app:connectedDebugAndroidTest
|
|
||||||
|
|
||||||
- name: (Fail-only) upload test report
|
- name: (Fail-only) upload test report
|
||||||
if: failure()
|
if: failure()
|
||||||
|
|
2
.idea/gradle.xml
generated
|
@ -13,6 +13,8 @@
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/data" />
|
<option value="$PROJECT_DIR$/data" />
|
||||||
|
<option value="$PROJECT_DIR$/lobsters-api" />
|
||||||
|
<option value="$PROJECT_DIR$/model" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
|
3
README.md
Normal file
|
@ -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).
|
|
@ -4,9 +4,10 @@ plugins {
|
||||||
id 'dagger.hilt.android.plugin'
|
id 'dagger.hilt.android.plugin'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||||
android {
|
android {
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "dev.msfjarvis.todo"
|
applicationId "dev.msfjarvis.lobsters"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,6 +26,21 @@ android {
|
||||||
kotlinCompilerVersion "${kotlin_version}"
|
kotlinCompilerVersion "${kotlin_version}"
|
||||||
kotlinCompilerExtensionVersion "${compose_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 {
|
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
|
||||||
|
@ -37,7 +53,9 @@ dependencies {
|
||||||
|
|
||||||
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
|
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
|
||||||
implementation(project(":data"))
|
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.appcompat:appcompat:1.3.0-alpha02'
|
||||||
implementation "androidx.compose.foundation:foundation:$compose_version"
|
implementation "androidx.compose.foundation:foundation:$compose_version"
|
||||||
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
|
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
|
||||||
|
|
|
@ -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<TodoItem>()
|
|
||||||
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<TodoItem>())
|
|
||||||
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<TodoItem>())
|
|
||||||
TodoTheme {
|
|
||||||
TodoApp(
|
|
||||||
items,
|
|
||||||
items::add,
|
|
||||||
items::remove,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onNodeWithText("Item 1").assertDoesNotExist()
|
|
||||||
onNodeWithTag("fab").performClick()
|
|
||||||
onNodeWithTag("add_button").performClick()
|
|
||||||
onNodeWithText("Item 1").assertDoesNotExist()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +1,21 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="dev.msfjarvis.todo">
|
package="dev.msfjarvis.lobsters">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".Application"
|
android:name=".Application"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@drawable/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.ComposeToDo">
|
android:theme="@style/Theme.Lobsters">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/Theme.ComposeToDo.NoActionBar">
|
android:theme="@style/Theme.Lobsters.NoActionBar">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
@ -22,4 +24,4 @@
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.msfjarvis.todo
|
package dev.msfjarvis.lobsters
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
81
app/src/main/java/dev/msfjarvis/lobsters/MainActivity.kt
Normal file
|
@ -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<UrlLauncher> { 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<LobstersPost>()
|
||||||
|
coroutineScope.launch {
|
||||||
|
apiClient.getHottestPosts().enqueue(object : Callback<List<LobstersPost>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<List<LobstersPost>>,
|
||||||
|
response: Response<List<LobstersPost>>
|
||||||
|
) {
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
response.body()?.let { posts.addAll(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<LobstersPost>>, t: Throwable) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
LobstersApp(posts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LobstersApp(
|
||||||
|
items: List<LobstersPost>,
|
||||||
|
) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dev.msfjarvis.todo.compose.utils
|
package dev.msfjarvis.lobsters.compose.utils
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Icon
|
import androidx.compose.foundation.Icon
|
17
app/src/main/java/dev/msfjarvis/lobsters/di/ApiModule.kt
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.msfjarvis.todo.di
|
package dev.msfjarvis.lobsters.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
@ -6,8 +6,8 @@ import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.components.ActivityComponent
|
import dagger.hilt.android.components.ActivityComponent
|
||||||
import dagger.hilt.android.qualifiers.ActivityContext
|
import dagger.hilt.android.qualifiers.ActivityContext
|
||||||
import dev.msfjarvis.todo.urllauncher.UrlLauncher
|
import dev.msfjarvis.lobsters.urllauncher.UrlLauncher
|
||||||
import dev.msfjarvis.todo.urllauncher.UrlLauncherImpl
|
import dev.msfjarvis.lobsters.urllauncher.UrlLauncherImpl
|
||||||
|
|
||||||
@InstallIn(ActivityComponent::class)
|
@InstallIn(ActivityComponent::class)
|
||||||
@Module
|
@Module
|
95
app/src/main/java/dev/msfjarvis/lobsters/ui/LobstersItem.kt
Normal file
|
@ -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 = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
app/src/main/java/dev/msfjarvis/lobsters/ui/Theme.kt
Normal file
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.msfjarvis.todo.urllauncher
|
package dev.msfjarvis.lobsters.urllauncher
|
||||||
|
|
||||||
interface UrlLauncher {
|
interface UrlLauncher {
|
||||||
fun launch(url: String)
|
fun launch(url: String)
|
|
@ -1,4 +1,4 @@
|
||||||
package dev.msfjarvis.todo.urllauncher
|
package dev.msfjarvis.lobsters.urllauncher
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
|
@ -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<UrlLauncher> { 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<TodoItem>,
|
|
||||||
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<Boolean>,
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <T> AnimatedSwipeDismiss(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
item: T,
|
|
||||||
background: @Composable (isDismissed: Boolean) -> Unit,
|
|
||||||
content: @Composable (isDismissed: Boolean) -> Unit,
|
|
||||||
directions: Set<DismissDirection> = 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) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<TodoItem>,
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:aapt="http://schemas.android.com/aapt"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
|
||||||
<aapt:attr name="android:fillColor">
|
|
||||||
<gradient
|
|
||||||
android:endX="85.84757"
|
|
||||||
android:endY="92.4963"
|
|
||||||
android:startX="42.9492"
|
|
||||||
android:startY="49.59793"
|
|
||||||
android:type="linear">
|
|
||||||
<item
|
|
||||||
android:color="#44000000"
|
|
||||||
android:offset="0.0" />
|
|
||||||
<item
|
|
||||||
android:color="#00000000"
|
|
||||||
android:offset="1.0" />
|
|
||||||
</gradient>
|
|
||||||
</aapt:attr>
|
|
||||||
</path>
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:fillType="nonZero"
|
|
||||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
|
||||||
android:strokeWidth="1"
|
|
||||||
android:strokeColor="#00000000" />
|
|
||||||
</vector>
|
|
BIN
app/src/main/res/drawable/ic_launcher.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
|
@ -1,170 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#3DDC84"
|
|
||||||
android:pathData="M0,0h108v108h-108z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M9,0L9,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,0L19,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,0L29,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,0L39,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,0L49,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,0L59,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,0L69,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,0L79,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M89,0L89,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M99,0L99,108"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,9L108,9"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,19L108,19"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,29L108,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,39L108,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,49L108,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,59L108,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,69L108,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,79L108,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,89L108,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M0,99L108,99"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,29L89,29"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,39L89,39"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,49L89,49"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,59L89,59"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,69L89,69"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M19,79L89,79"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M29,19L29,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M39,19L39,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M49,19L49,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M59,19L59,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M69,19L69,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#00000000"
|
|
||||||
android:pathData="M79,19L79,89"
|
|
||||||
android:strokeWidth="0.8"
|
|
||||||
android:strokeColor="#33FFFFFF" />
|
|
||||||
</vector>
|
|
BIN
app/src/main/res/drawable/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
|
@ -1,5 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
Before Width: | Height: | Size: 3.5 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 4.8 KiB |
Before Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 7.7 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 16 KiB |
|
@ -1,6 +1,6 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.ComposeToDo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Lobsters" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/purple_200</item>
|
<item name="colorPrimary">@color/purple_200</item>
|
||||||
<item name="colorPrimaryVariant">@color/black</item>
|
<item name="colorPrimaryVariant">@color/black</item>
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Compose-ToDo</string>
|
<string name="app_name">lobste.rs</string>
|
||||||
</resources>
|
</resources>
|
|
@ -1,6 +1,6 @@
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="Theme.ComposeToDo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
<style name="Theme.Lobsters" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
<!-- Primary brand color. -->
|
<!-- Primary brand color. -->
|
||||||
<item name="colorPrimary">@color/white</item>
|
<item name="colorPrimary">@color/white</item>
|
||||||
<item name="colorPrimaryVariant">@color/white</item>
|
<item name="colorPrimaryVariant">@color/white</item>
|
||||||
|
@ -15,12 +15,12 @@
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.ComposeToDo.NoActionBar">
|
<style name="Theme.Lobsters.NoActionBar">
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.ComposeToDo.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
<style name="Theme.Lobsters.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
<style name="Theme.ComposeToDo.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
<style name="Theme.Lobsters.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||||
</resources>
|
</resources>
|
|
@ -12,7 +12,7 @@ buildscript {
|
||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,2 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest package="dev.msfjarvis.todo.data" />
|
||||||
package="dev.msfjarvis.todo.data">
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package dev.msfjarvis.todo.data.source
|
package dev.msfjarvis.lobsters.data.source
|
||||||
|
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
|
@ -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")),
|
|
||||||
)
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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<List<TodoItem>>
|
|
||||||
|
|
||||||
@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<TodoItem>)
|
|
||||||
|
|
||||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
abstract suspend fun update(entity: TodoItem)
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
abstract suspend fun delete(entity: TodoItem): Int
|
|
||||||
}
|
|
1
lobsters-api/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
17
lobsters-api/build.gradle
Normal file
|
@ -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"
|
||||||
|
}
|
2
lobsters-api/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="dev.msfjarvis.lobsters.api" />
|
|
@ -0,0 +1,14 @@
|
||||||
|
package dev.msfjarvis.lobsters.api
|
||||||
|
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||||
|
|
||||||
|
object ApiClient {
|
||||||
|
inline fun <reified T> getClient(baseUrl: String): T {
|
||||||
|
return Retrofit.Builder()
|
||||||
|
.baseUrl(baseUrl)
|
||||||
|
.addConverterFactory(MoshiConverterFactory.create())
|
||||||
|
.build()
|
||||||
|
.create(T::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<List<dev.msfjarvis.lobsters.model.LobstersPost>>
|
||||||
|
}
|
|
@ -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<LobstersApi>("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<List<dev.msfjarvis.lobsters.model.LobstersPost>> {
|
||||||
|
override fun onResponse(
|
||||||
|
call: Call<List<dev.msfjarvis.lobsters.model.LobstersPost>>,
|
||||||
|
response: Response<List<dev.msfjarvis.lobsters.model.LobstersPost>>
|
||||||
|
) {
|
||||||
|
val posts = response.body()
|
||||||
|
require(posts != null)
|
||||||
|
assertEquals(25, posts.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<dev.msfjarvis.lobsters.model.LobstersPost>>, t: Throwable) {
|
||||||
|
fail("Call cannot fail in tests")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
webServer.shutdown()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
1
lobsters-api/src/test/resources/hottest.json
Normal file
1
model/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
12
model/build.gradle
Normal file
|
@ -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"
|
||||||
|
}
|
2
model/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="dev.msfjarvis.lobsters.model" />
|
|
@ -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
|
||||||
|
)
|
|
@ -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<String>
|
||||||
|
)
|
|
@ -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<KeybaseSignature>?
|
||||||
|
)
|
|
@ -1,3 +1,5 @@
|
||||||
rootProject.name = "Compose-ToDo"
|
rootProject.name = "lobste.rs"
|
||||||
include ':app'
|
include ':app'
|
||||||
include ':data'
|
include ':data'
|
||||||
|
include ':lobsters-api'
|
||||||
|
include ':model'
|
||||||
|
|