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
|
||||
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
|
@ -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
|
@ -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'
|
||||
}
|
||||
|
||||
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"
|
||||
|
|
|
@ -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"?>
|
||||
<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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package dev.msfjarvis.todo
|
||||
package dev.msfjarvis.lobsters
|
||||
|
||||
import android.app.Application
|
||||
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
|
||||
* 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
|
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 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
|
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 {
|
||||
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.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">
|
||||
<!-- 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>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">Compose-ToDo</string>
|
||||
<string name="app_name">lobste.rs</string>
|
||||
</resources>
|
|
@ -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>
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
|
@ -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 ':data'
|
||||
include ':lobsters-api'
|
||||
include ':model'
|
||||
|
|