Merge pull request #20 from msfjarvis/pivot-to-lobsters

This commit is contained in:
Harsh Shandilya 2020-09-23 04:03:26 +05:30 committed by GitHub
commit a3b1e02783
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 470 additions and 837 deletions

View file

@ -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()

2
.idea/gradle.xml generated
View file

@ -13,6 +13,8 @@
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/data" />
<option value="$PROJECT_DIR$/lobsters-api" />
<option value="$PROJECT_DIR$/model" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

3
README.md Normal file
View 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).

View file

@ -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"

View file

@ -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()
}
}
}

View file

@ -1,19 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<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
android:name=".Application"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@drawable/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ComposeToDo">
android:theme="@style/Theme.Lobsters">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/Theme.ComposeToDo.NoActionBar">
android:theme="@style/Theme.Lobsters.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -22,4 +24,4 @@
</activity>
</application>
</manifest>
</manifest>

View file

@ -1,4 +1,4 @@
package dev.msfjarvis.todo
package dev.msfjarvis.lobsters
import android.app.Application
import dagger.hilt.android.HiltAndroidApp

View 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)
}
}
}
)
}

View file

@ -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

View 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")
}
}

View file

@ -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

View 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 = {})
}
}
}

View 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,
)
}

View file

@ -1,4 +1,4 @@
package dev.msfjarvis.todo.urllauncher
package dev.msfjarvis.lobsters.urllauncher
interface UrlLauncher {
fun launch(url: String)

View file

@ -1,4 +1,4 @@
package dev.msfjarvis.todo.urllauncher
package dev.msfjarvis.lobsters.urllauncher
import android.content.Context
import android.content.Intent

View file

@ -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,
)
}
}

View file

@ -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()
}
}

View file

@ -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) }
)
}
}

View file

@ -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),
)
}
}

View file

@ -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),
)
},
)
}
}

View file

@ -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,
)
}

View file

@ -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,
)
},
)
}
}
}

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View file

@ -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>

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ComposeToDo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.Lobsters" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/black</item>

View file

@ -1,3 +1,3 @@
<resources>
<string name="app_name">Compose-ToDo</string>
<string name="app_name">lobste.rs</string>
</resources>

View file

@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.ComposeToDo" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<style name="Theme.Lobsters" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/white</item>
<item name="colorPrimaryVariant">@color/white</item>
@ -15,12 +15,12 @@
<!-- Customize your theme here. -->
</style>
<style name="Theme.ComposeToDo.NoActionBar">
<style name="Theme.Lobsters.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</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>

View file

@ -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"
}

View file

@ -1,5 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="dev.msfjarvis.todo.data">
</manifest>
<manifest package="dev.msfjarvis.todo.data" />

View file

@ -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

View file

@ -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")),
)

View file

@ -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
}

View file

@ -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
View file

@ -0,0 +1 @@
/build

17
lobsters-api/build.gradle Normal file
View 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"
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="dev.msfjarvis.lobsters.api" />

View file

@ -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)
}
}

View file

@ -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>>
}

View file

@ -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()
}
}

View file

@ -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())
}
}

File diff suppressed because one or more lines are too long

1
model/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

12
model/build.gradle Normal file
View 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"
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="dev.msfjarvis.lobsters.model" />

View file

@ -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
)

View file

@ -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>
)

View file

@ -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>?
)

View file

@ -1,3 +1,5 @@
rootProject.name = "Compose-ToDo"
rootProject.name = "lobste.rs"
include ':app'
include ':data'
include ':lobsters-api'
include ':model'