all: reformat with ktfmt google style

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2021-04-02 13:05:08 +05:30
parent 8448910628
commit db07a12be5
54 changed files with 496 additions and 656 deletions

View file

@ -4,16 +4,12 @@ import dev.msfjarvis.lobsters.model.LobstersPost
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.Query import retrofit2.http.Query
/** /** Simple interface defining an API for lobste.rs */
* Simple interface defining an API for lobste.rs
*/
interface LobstersApi { interface LobstersApi {
@GET("hottest.json") @GET("hottest.json") suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
@GET("newest.json") @GET("newest.json") suspend fun getNewestPosts(@Query("page") page: Int): List<LobstersPost>
suspend fun getNewestPosts(@Query("page") page: Int): List<LobstersPost>
companion object { companion object {
const val BASE_URL = "https://lobste.rs" const val BASE_URL = "https://lobste.rs"

View file

@ -5,8 +5,6 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class KeybaseSignature( class KeybaseSignature(
@Json(name = "kb_username") @Json(name = "kb_username") val kbUsername: String,
val kbUsername: String, @Json(name = "sig_hash") val sigHash: String,
@Json(name = "sig_hash")
val sigHash: String,
) )

View file

@ -5,22 +5,16 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class LobstersPost( class LobstersPost(
@Json(name = "short_id") @Json(name = "short_id") val shortId: String,
val shortId: String, @Json(name = "short_id_url") val shortIdUrl: String,
@Json(name = "short_id_url") @Json(name = "created_at") val createdAt: String,
val shortIdUrl: String,
@Json(name = "created_at")
val createdAt: String,
val title: String, val title: String,
val url: String, val url: String,
val score: Long, val score: Long,
val flags: Long, val flags: Long,
@Json(name = "comment_count") @Json(name = "comment_count") val commentCount: Long,
val commentCount: Long,
val description: String, val description: String,
@Json(name = "comments_url") @Json(name = "comments_url") val commentsUrl: String,
val commentsUrl: String, @Json(name = "submitter_user") val submitterUser: Submitter,
@Json(name = "submitter_user")
val submitterUser: Submitter,
val tags: List<String>, val tags: List<String>,
) )

View file

@ -6,22 +6,14 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
class Submitter( class Submitter(
val username: String, val username: String,
@Json(name = "created_at") @Json(name = "created_at") val createdAt: String,
val createdAt: String, @Json(name = "is_admin") val isAdmin: Boolean,
@Json(name = "is_admin")
val isAdmin: Boolean,
val about: String, val about: String,
@Json(name = "is_moderator") @Json(name = "is_moderator") val isModerator: Boolean,
val isModerator: Boolean,
val karma: Long = 0, val karma: Long = 0,
@Json(name = "avatar_url") @Json(name = "avatar_url") val avatarUrl: String,
val avatarUrl: String, @Json(name = "invited_by_user") val invitedByUser: String,
@Json(name = "invited_by_user") @Json(name = "github_username") val githubUsername: String? = null,
val invitedByUser: String, @Json(name = "twitter_username") val twitterUsername: String? = null,
@Json(name = "github_username") @Json(name = "keybase_signatures") val keybaseSignatures: List<KeybaseSignature> = emptyList(),
val githubUsername: String? = null,
@Json(name = "twitter_username")
val twitterUsername: String? = null,
@Json(name = "keybase_signatures")
val keybaseSignatures: List<KeybaseSignature> = emptyList(),
) )

View file

@ -22,26 +22,26 @@ class LobstersApiTest {
companion object { companion object {
private val webServer = MockWebServer() private val webServer = MockWebServer()
private val apiData = TestUtils.getJson("hottest.json") private val apiData = TestUtils.getJson("hottest.json")
private val moshi = Moshi.Builder() private val moshi = Moshi.Builder().build()
.build() private val okHttp = OkHttpClient.Builder().build()
private val okHttp = OkHttpClient.Builder() private val retrofit =
.build() Retrofit.Builder()
private val retrofit = Retrofit.Builder() .client(okHttp)
.client(okHttp) .baseUrl("http://localhost:8080/")
.baseUrl("http://localhost:8080/") .addConverterFactory(MoshiConverterFactory.create(moshi))
.addConverterFactory(MoshiConverterFactory.create(moshi)) .build()
.build()
private val apiClient = retrofit.create<LobstersApi>() private val apiClient = retrofit.create<LobstersApi>()
@JvmStatic @JvmStatic
@BeforeClass @BeforeClass
fun setUp() { fun setUp() {
webServer.start(8080) webServer.start(8080)
webServer.dispatcher = object : Dispatcher() { webServer.dispatcher =
override fun dispatch(request: RecordedRequest): MockResponse { object : Dispatcher() {
return MockResponse().setBody(apiData).setResponseCode(200) override fun dispatch(request: RecordedRequest): MockResponse {
return MockResponse().setBody(apiData).setResponseCode(200)
}
} }
}
} }
@JvmStatic @JvmStatic
@ -60,18 +60,14 @@ class LobstersApiTest {
@Test @Test
fun `no moderator posts in test data`() = runBlocking { fun `no moderator posts in test data`() = runBlocking {
val posts = apiClient.getHottestPosts(1) val posts = apiClient.getHottestPosts(1)
val moderatorPosts = posts.asSequence() val moderatorPosts = posts.asSequence().filter { it.submitterUser.isModerator }.toSet()
.filter { it.submitterUser.isModerator }
.toSet()
assertTrue(moderatorPosts.isEmpty()) assertTrue(moderatorPosts.isEmpty())
} }
@Test @Test
fun `posts with no urls`() = runBlocking { fun `posts with no urls`() = runBlocking {
val posts = apiClient.getHottestPosts(1) val posts = apiClient.getHottestPosts(1)
val commentsOnlyPosts = posts.asSequence() val commentsOnlyPosts = posts.asSequence().filter { it.url.isEmpty() }.toSet()
.filter { it.url.isEmpty() }
.toSet()
assertEquals(2, commentsOnlyPosts.size) assertEquals(2, commentsOnlyPosts.size)
} }
} }

View file

@ -10,9 +10,7 @@ plugins {
`core-library-desugaring` `core-library-desugaring`
} }
repositories { repositories { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
android { android {
defaultConfig { defaultConfig {
@ -22,7 +20,6 @@ android {
} }
dependencies { dependencies {
kapt(Dependencies.AndroidX.Hilt.daggerCompiler) kapt(Dependencies.AndroidX.Hilt.daggerCompiler)
implementation(project(":api")) implementation(project(":api"))
implementation(project(":common")) implementation(project(":common"))

View file

@ -15,8 +15,7 @@ import org.junit.Test
@Ignore("Shot is broken yet again") @Ignore("Shot is broken yet again")
class LobstersTopBarTest : ScreenshotTest { class LobstersTopBarTest : ScreenshotTest {
@get:Rule @get:Rule val composeTestRule = createComposeRule()
val composeTestRule = createComposeRule()
@Test @Test
fun showsRefreshIconWhenOnHottestPostsScreen_DarkTheme() { fun showsRefreshIconWhenOnHottestPostsScreen_DarkTheme() {
@ -24,7 +23,7 @@ class LobstersTopBarTest : ScreenshotTest {
DarkTestTheme { DarkTestTheme {
LobstersTopAppBar( LobstersTopAppBar(
currentDestination = Destination.Hottest, currentDestination = Destination.Hottest,
toggleSortingOrder = { }, toggleSortingOrder = {},
) )
} }
} }
@ -38,7 +37,7 @@ class LobstersTopBarTest : ScreenshotTest {
LightTestTheme { LightTestTheme {
LobstersTopAppBar( LobstersTopAppBar(
currentDestination = Destination.Hottest, currentDestination = Destination.Hottest,
toggleSortingOrder = { }, toggleSortingOrder = {},
) )
} }
} }
@ -52,7 +51,7 @@ class LobstersTopBarTest : ScreenshotTest {
DarkTestTheme { DarkTestTheme {
LobstersTopAppBar( LobstersTopAppBar(
currentDestination = Destination.Saved, currentDestination = Destination.Saved,
toggleSortingOrder = { }, toggleSortingOrder = {},
) )
} }
} }
@ -66,7 +65,7 @@ class LobstersTopBarTest : ScreenshotTest {
LightTestTheme { LightTestTheme {
LobstersTopAppBar( LobstersTopAppBar(
currentDestination = Destination.Saved, currentDestination = Destination.Saved,
toggleSortingOrder = { }, toggleSortingOrder = {},
) )
} }
} }

View file

@ -14,7 +14,6 @@ import androidx.compose.ui.test.performClick
import com.karumi.shot.ScreenshotTest import com.karumi.shot.ScreenshotTest
import dev.msfjarvis.lobsters.ui.DarkTestTheme import dev.msfjarvis.lobsters.ui.DarkTestTheme
import dev.msfjarvis.lobsters.ui.main.LobstersBottomNav import dev.msfjarvis.lobsters.ui.main.LobstersBottomNav
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
import kotlin.test.Test import kotlin.test.Test
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
@ -22,8 +21,7 @@ import org.junit.Rule
@Ignore("Shot is broken yet again") @Ignore("Shot is broken yet again")
class LobstersBottomNavTest : ScreenshotTest { class LobstersBottomNavTest : ScreenshotTest {
@get:Rule @get:Rule val composeTestRule = createComposeRule()
val composeTestRule = createComposeRule()
@Test @Test
fun bottomNavIsRenderedCorrectlyOnScreen() { fun bottomNavIsRenderedCorrectlyOnScreen() {
@ -31,7 +29,7 @@ class LobstersBottomNavTest : ScreenshotTest {
DarkTestTheme { DarkTestTheme {
LobstersBottomNav( LobstersBottomNav(
currentDestination = Destination.startDestination, currentDestination = Destination.startDestination,
navigateToDestination = { /*TODO*/ }, navigateToDestination = { /*TODO*/},
jumpToIndex = { _, _ -> }, jumpToIndex = { _, _ -> },
) )
} }
@ -59,8 +57,6 @@ class LobstersBottomNavTest : ScreenshotTest {
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap()) compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
} }
private fun selectNode(testTag: String) = composeTestRule private fun selectNode(testTag: String) =
.onNodeWithTag(testTag) composeTestRule.onNodeWithTag(testTag).assertHasClickAction().performClick()
.assertHasClickAction()
.performClick()
} }

View file

@ -19,21 +19,16 @@ import org.junit.Rule
@Ignore("Shot is broken yet again") @Ignore("Shot is broken yet again")
class HeaderTest : ScreenshotTest { class HeaderTest : ScreenshotTest {
@get:Rule @get:Rule val composeTestRule = createComposeRule()
val composeTestRule = createComposeRule()
@Test @Test
fun headerDoesNotHaveATransparentBackground() { fun headerDoesNotHaveATransparentBackground() {
composeTestRule.setContent { composeTestRule.setContent {
DarkTestTheme { DarkTestTheme {
Box( Box(
modifier = Modifier modifier =
.background(color = Color(0xffffff)) Modifier.background(color = Color(0xffffff)).fillMaxWidth().wrapContentHeight(),
.fillMaxWidth() ) { MonthHeader(month = Month.AUGUST) }
.wrapContentHeight(),
) {
MonthHeader(month = Month.AUGUST)
}
} }
} }
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap()) compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())

View file

@ -15,8 +15,7 @@ import org.junit.Rule
@Ignore("Shot is broken yet again") @Ignore("Shot is broken yet again")
class LobstersItemTest : ScreenshotTest { class LobstersItemTest : ScreenshotTest {
@get:Rule @get:Rule val composeTestRule = createComposeRule()
val composeTestRule = createComposeRule()
@Test @Test
fun singlePost() { fun singlePost() {
@ -24,9 +23,9 @@ class LobstersItemTest : ScreenshotTest {
DarkTestTheme { DarkTestTheme {
LobstersItem( LobstersItem(
post = TEST_POST, post = TEST_POST,
viewPost = { /*TODO*/ }, viewPost = {},
viewComments = { /*TODO*/ }, viewComments = {},
toggleSave = { /*TODO*/ }, toggleSave = {},
isSaved = true, isSaved = true,
) )
} }
@ -42,9 +41,9 @@ class LobstersItemTest : ScreenshotTest {
items(10) { items(10) {
LobstersItem( LobstersItem(
post = TEST_POST, post = TEST_POST,
viewPost = { /*TODO*/ }, viewPost = {},
viewComments = { /*TODO*/ }, viewComments = {},
toggleSave = { /*TODO*/ }, toggleSave = {},
isSaved = true, isSaved = true,
) )
} }
@ -62,9 +61,9 @@ class LobstersItemTest : ScreenshotTest {
items(10) { items(10) {
LobstersItem( LobstersItem(
post = TEST_POST.copy(tags = listOf("openbsd", "linux")), post = TEST_POST.copy(tags = listOf("openbsd", "linux")),
viewPost = { /*TODO*/ }, viewPost = {},
viewComments = { /*TODO*/ }, viewComments = {},
toggleSave = { /*TODO*/ }, toggleSave = {},
isSaved = true, isSaved = true,
) )
} }

View file

@ -25,13 +25,11 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
private const val MAX_OFFSET = 400f private const val MAX_OFFSET = 400f
private const val MIN_REFRESH_OFFSET = 250f private const val MIN_REFRESH_OFFSET = 250f
private const val PERCENT_INDICATOR_PROGRESS_ON_DRAG = 0.85f private const val PERCENT_INDICATOR_PROGRESS_ON_DRAG = 0.85f
private const val BASE_OFFSET = -48 private const val BASE_OFFSET = -48
/** /**
* A layout composable with [content]. * A layout composable with [content].
* *
@ -78,26 +76,30 @@ fun PullToRefresh(
finishedListener = { finishedListener = {
indicatorOffset = 0f indicatorOffset = 0f
isFinishingRefresh = false isFinishingRefresh = false
}) }
)
val offsetAnimation by animateFloatAsState( val offsetAnimation by animateFloatAsState(
targetValue = if (isRefreshing || isFinishingRefresh) { targetValue =
indicatorOffset - minRefreshOffset if (isRefreshing || isFinishingRefresh) {
} else { indicatorOffset - minRefreshOffset
0f } else {
} 0f
}
) )
val resettingScrollOffsetAnimation by animateFloatAsState( val resettingScrollOffsetAnimation by animateFloatAsState(
targetValue = if (isResettingScroll) { targetValue =
scrollToReset if (isResettingScroll) {
} else { scrollToReset
0f } else {
}, 0f
},
finishedListener = { finishedListener = {
if (isResettingScroll) { if (isResettingScroll) {
indicatorOffset = 0f indicatorOffset = 0f
isResettingScroll = false isResettingScroll = false
} }
}) }
)
if (isResettingScroll) { if (isResettingScroll) {
indicatorOffset -= resettingScrollOffsetAnimation indicatorOffset -= resettingScrollOffsetAnimation
@ -110,100 +112,102 @@ fun PullToRefresh(
isRefreshingInternal = true isRefreshingInternal = true
} }
val nestedScrollConnection = object : NestedScrollConnection { val nestedScrollConnection =
object : NestedScrollConnection {
override fun onPostScroll( override fun onPostScroll(
consumed: Offset, consumed: Offset,
available: Offset, available: Offset,
source: NestedScrollSource source: NestedScrollSource
): Offset { ): Offset {
if (!isRefreshing && source == NestedScrollSource.Drag) { if (!isRefreshing && source == NestedScrollSource.Drag) {
val diff = if (indicatorOffset + available.y > maxOffset) { val diff =
available.y - (indicatorOffset + available.y - maxOffset) if (indicatorOffset + available.y > maxOffset) {
} else if (indicatorOffset + available.y < 0) { available.y - (indicatorOffset + available.y - maxOffset)
0f } else if (indicatorOffset + available.y < 0) {
} else { 0f
available.y } else {
available.y
}
indicatorOffset += diff
return Offset(0f, diff)
} }
indicatorOffset += diff return super.onPostScroll(consumed, available, source)
return Offset(0f, diff)
} }
return super.onPostScroll(consumed, available, source)
}
override fun onPreScroll( override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
available: Offset, if (!isRefreshing && source == NestedScrollSource.Drag) {
source: NestedScrollSource if (available.y < 0 && indicatorOffset > 0) {
): Offset { val diff =
if (!isRefreshing && source == NestedScrollSource.Drag) { if (indicatorOffset + available.y < 0) {
if (available.y < 0 && indicatorOffset > 0) { indicatorOffset = 0f
val diff = if (indicatorOffset + available.y < 0) { indicatorOffset
indicatorOffset = 0f } else {
indicatorOffset indicatorOffset += available.y
available.y
}
isFinishingRefresh = false
return Offset.Zero.copy(y = diff)
}
}
return super.onPreScroll(available, source)
}
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
if (!isRefreshing) {
if (indicatorOffset > minRefreshOffset) {
onRefresh()
isRefreshingInternal = true
} else { } else {
indicatorOffset += available.y isResettingScroll = true
available.y scrollToReset = indicatorOffset
} }
isFinishingRefresh = false
return Offset.Zero.copy(y = diff)
} }
return super.onPostFling(consumed, available)
} }
return super.onPreScroll(available, source)
} }
override suspend fun onPostFling(
consumed: Velocity,
available: Velocity
): Velocity {
if (!isRefreshing) {
if (indicatorOffset > minRefreshOffset) {
onRefresh()
isRefreshingInternal = true
} else {
isResettingScroll = true
scrollToReset = indicatorOffset
}
}
return super.onPostFling(consumed, available)
}
}
Box( Box(
modifier = CombinedModifier( modifier =
inner = Modifier CombinedModifier(
.nestedScroll(nestedScrollConnection) inner = Modifier.nestedScroll(nestedScrollConnection).clip(RectangleShape),
.clip(RectangleShape), outer = modifier
outer = modifier )
)
) { ) {
content() content()
val offsetPx = if (isRefreshing || isFinishingRefresh) { val offsetPx =
offsetAnimation if (isRefreshing || isFinishingRefresh) {
} else { offsetAnimation
0f } else {
} 0f
val absoluteOffset = BASE_OFFSET.dp + with(LocalDensity.current) { }
val diffedOffset = indicatorOffset - offsetPx val absoluteOffset =
val calculated = calculateAbsoluteOffset(diffedOffset, MAX_OFFSET) BASE_OFFSET.dp +
calculated.toDp() with(LocalDensity.current) {
} val diffedOffset = indicatorOffset - offsetPx
val progressFromOffset = with(LocalDensity.current) { val calculated = calculateAbsoluteOffset(diffedOffset, MAX_OFFSET)
val coeff = MAX_OFFSET / (MAX_OFFSET - BASE_OFFSET) calculated.toDp()
(indicatorOffset - BASE_OFFSET) / maxOffset * coeff }
} val progressFromOffset =
with(LocalDensity.current) {
val coeff = MAX_OFFSET / (MAX_OFFSET - BASE_OFFSET)
(indicatorOffset - BASE_OFFSET) / maxOffset * coeff
}
PullToRefreshProgressIndicator( PullToRefreshProgressIndicator(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth()
.absoluteOffset(y = absoluteOffset) .absoluteOffset(y = absoluteOffset)
.scale(scaleAnimation) .scale(scaleAnimation)
.rotate(indicatorOffset / MAX_OFFSET * 180 + 110), .rotate(indicatorOffset / MAX_OFFSET * 180 + 110),
progressColor = progressColor, progressColor = progressColor,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
progress = when { progress =
!isRefreshing && !isFinishingRefresh -> progressFromOffset * PERCENT_INDICATOR_PROGRESS_ON_DRAG when {
else -> null !isRefreshing && !isFinishingRefresh ->
}, progressFromOffset * PERCENT_INDICATOR_PROGRESS_ON_DRAG
else -> null
},
) )
} }
} }

View file

@ -27,11 +27,9 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
private val CircularIndicatorDiameter = 40.dp private val CircularIndicatorDiameter = 40.dp
private const val strokeWidthPx = 2.5f private const val strokeWidthPx = 2.5f
@Composable @Composable
internal fun PullToRefreshProgressIndicator( internal fun PullToRefreshProgressIndicator(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -39,22 +37,15 @@ internal fun PullToRefreshProgressIndicator(
backgroundColor: Color, backgroundColor: Color,
progress: Float? = null progress: Float? = null
) { ) {
Row( Row(modifier = modifier, horizontalArrangement = Arrangement.Center) {
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
Card( Card(
modifier = Modifier modifier = Modifier.width(CircularIndicatorDiameter).height(CircularIndicatorDiameter),
.width(CircularIndicatorDiameter)
.height(CircularIndicatorDiameter),
shape = CircleShape, shape = CircleShape,
elevation = 6.dp, elevation = 6.dp,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
) { ) {
val padding = Modifier.padding(8.dp) val padding = Modifier.padding(8.dp)
val strokeWidth = with(LocalDensity.current) { val strokeWidth = with(LocalDensity.current) { (strokeWidthPx * this.density).toDp() }
(strokeWidthPx * this.density).toDp()
}
if (progress == null) { if (progress == null) {
CircularProgressIndicator( CircularProgressIndicator(
@ -74,7 +65,6 @@ internal fun PullToRefreshProgressIndicator(
} }
} }
@Composable @Composable
fun ProgressIndicatorWithArrow( fun ProgressIndicatorWithArrow(
progress: Float, progress: Float,
@ -83,47 +73,41 @@ fun ProgressIndicatorWithArrow(
strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth, strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth,
) { ) {
val strokeWidthPx = with(LocalDensity.current) { val strokeWidthPx = with(LocalDensity.current) { strokeWidth.toPx() }
strokeWidth.toPx()
}
val arrowWidth = 2.5f * strokeWidthPx * (0.5f + progress * 0.5f) val arrowWidth = 2.5f * strokeWidthPx * (0.5f + progress * 0.5f)
val stroke = Stroke(width = strokeWidthPx, cap = StrokeCap.Butt) val stroke = Stroke(width = strokeWidthPx, cap = StrokeCap.Butt)
val diameterOffset = stroke.width / 2 val diameterOffset = stroke.width / 2
val arrowPath = Path().apply { val arrowPath =
moveTo(0f, -arrowWidth) Path().apply {
lineTo(arrowWidth, 0f) moveTo(0f, -arrowWidth)
lineTo(0f, arrowWidth) lineTo(arrowWidth, 0f)
close() lineTo(0f, arrowWidth)
} close()
}
Box(modifier = modifier) { Box(modifier = modifier) {
Canvas(modifier = Modifier Canvas(
.fillMaxWidth() modifier = Modifier.fillMaxWidth().fillMaxHeight(),
.fillMaxHeight(), onDraw = { onDraw = {
val arcDimen = size.width - 2 * diameterOffset val arcDimen = size.width - 2 * diameterOffset
withTransform({ withTransform({
translate(top = strokeWidthPx / 2, left = size.width / 2) translate(top = strokeWidthPx / 2, left = size.width / 2)
rotate( rotate(
degrees = progress * 360, degrees = progress * 360,
pivot = Offset(x = 0f, y = size.height / 2 - diameterOffset) pivot = Offset(x = 0f, y = size.height / 2 - diameterOffset)
) )
}) { }) { drawPath(path = arrowPath, color = color) }
drawPath(
path = arrowPath, drawArc(
color = color color = color,
startAngle = -90f,
sweepAngle = 360 * progress,
useCenter = false,
topLeft = Offset(diameterOffset, diameterOffset),
size = Size(arcDimen, arcDimen),
style = stroke
) )
} }
)
drawArc(
color = color,
startAngle = -90f,
sweepAngle = 360 * progress,
useCenter = false,
topLeft = Offset(diameterOffset, diameterOffset),
size = Size(arcDimen, arcDimen),
style = stroke
)
})
} }
} }

View file

@ -3,5 +3,4 @@ package dev.msfjarvis.lobsters
import android.app.Application import android.app.Application
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp @HiltAndroidApp class ClawApplication : Application()
class ClawApplication : Application()

View file

@ -8,7 +8,9 @@ import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
class ClawPreferences @Inject constructor( class ClawPreferences
@Inject
constructor(
private val dataStore: DataStore<Preferences>, private val dataStore: DataStore<Preferences>,
) { ) {
private val sortKey = booleanPreferencesKey("post_sorting_order") private val sortKey = booleanPreferencesKey("post_sorting_order")
@ -17,8 +19,6 @@ class ClawPreferences @Inject constructor(
get() = dataStore.data.map { preferences -> preferences[sortKey] ?: false } get() = dataStore.data.map { preferences -> preferences[sortKey] ?: false }
suspend fun toggleSortingOrder() { suspend fun toggleSortingOrder() {
dataStore.edit { preferences -> dataStore.edit { preferences -> preferences[sortKey] = (preferences[sortKey] ?: false).not() }
preferences[sortKey] = (preferences[sortKey] ?: false).not()
}
} }
} }

View file

@ -5,7 +5,8 @@ import androidx.paging.PagingState
import dev.msfjarvis.lobsters.data.repo.LobstersRepository import dev.msfjarvis.lobsters.data.repo.LobstersRepository
import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.model.LobstersPost
class HottestPostsPagingSource constructor( class HottestPostsPagingSource
constructor(
private val lobstersRepository: LobstersRepository, private val lobstersRepository: LobstersRepository,
) : PagingSource<Int, LobstersPost>() { ) : PagingSource<Int, LobstersPost>() {

View file

@ -5,7 +5,8 @@ import androidx.paging.PagingState
import dev.msfjarvis.lobsters.data.repo.LobstersRepository import dev.msfjarvis.lobsters.data.repo.LobstersRepository
import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.model.LobstersPost
class NewestPostsPagingSource constructor( class NewestPostsPagingSource
constructor(
private val lobstersRepository: LobstersRepository, private val lobstersRepository: LobstersRepository,
) : PagingSource<Int, LobstersPost>() { ) : PagingSource<Int, LobstersPost>() {

View file

@ -2,14 +2,15 @@ package dev.msfjarvis.lobsters.data.repo
import dev.msfjarvis.lobsters.data.api.LobstersApi import dev.msfjarvis.lobsters.data.api.LobstersApi
import dev.msfjarvis.lobsters.data.local.SavedPost import dev.msfjarvis.lobsters.data.local.SavedPost
import dev.msfjarvis.lobsters.model.LobstersPost
import dev.msfjarvis.lobsters.database.LobstersDatabase import dev.msfjarvis.lobsters.database.LobstersDatabase
import dev.msfjarvis.lobsters.model.LobstersPost
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class LobstersRepository constructor( class LobstersRepository
constructor(
private val lobstersApi: LobstersApi, private val lobstersApi: LobstersApi,
private val lobstersDatabase: LobstersDatabase, private val lobstersDatabase: LobstersDatabase,
) { ) {
@ -26,13 +27,15 @@ class LobstersRepository constructor(
return savedPostsCache.values.toList() return savedPostsCache.values.toList()
} }
suspend fun fetchHottestPosts(page: Int): List<LobstersPost> = withContext(Dispatchers.IO) { suspend fun fetchHottestPosts(page: Int): List<LobstersPost> =
return@withContext lobstersApi.getHottestPosts(page) withContext(Dispatchers.IO) {
} return@withContext lobstersApi.getHottestPosts(page)
}
suspend fun fetchNewestPosts(page: Int): List<LobstersPost> = withContext(Dispatchers.IO) { suspend fun fetchNewestPosts(page: Int): List<LobstersPost> =
return@withContext lobstersApi.getNewestPosts(page) withContext(Dispatchers.IO) {
} return@withContext lobstersApi.getNewestPosts(page)
}
// https://issuetracker.google.com/issues/181221325 // https://issuetracker.google.com/issues/181221325
@Suppress("NewApi") @Suppress("NewApi")
@ -40,27 +43,28 @@ class LobstersRepository constructor(
if (_isCacheReady.value) return if (_isCacheReady.value) return
val posts = getSavedPosts() val posts = getSavedPosts()
posts.forEach { posts.forEach { savedPostsCache[it.shortId] = it }
savedPostsCache[it.shortId] = it
}
_isCacheReady.value = true _isCacheReady.value = true
} }
private suspend fun getSavedPosts(): List<SavedPost> = withContext(Dispatchers.IO) { private suspend fun getSavedPosts(): List<SavedPost> =
return@withContext lobstersDatabase.savedPostQueries.selectAllPosts().executeAsList() withContext(Dispatchers.IO) {
} return@withContext lobstersDatabase.savedPostQueries.selectAllPosts().executeAsList()
}
suspend fun addPost(post: SavedPost) = withContext(Dispatchers.IO) { suspend fun addPost(post: SavedPost) =
if (!savedPostsCache.containsKey(post.shortId)) { withContext(Dispatchers.IO) {
savedPostsCache.putIfAbsent(post.shortId, post) if (!savedPostsCache.containsKey(post.shortId)) {
lobstersDatabase.savedPostQueries.insertOrReplacePost(post) savedPostsCache.putIfAbsent(post.shortId, post)
lobstersDatabase.savedPostQueries.insertOrReplacePost(post)
}
} }
}
suspend fun removePost(post: SavedPost) = withContext(Dispatchers.IO) { suspend fun removePost(post: SavedPost) =
if (savedPostsCache.containsKey(post.shortId)) { withContext(Dispatchers.IO) {
savedPostsCache.remove(post.shortId) if (savedPostsCache.containsKey(post.shortId)) {
lobstersDatabase.savedPostQueries.deletePost(post.shortId) savedPostsCache.remove(post.shortId)
lobstersDatabase.savedPostQueries.deletePost(post.shortId)
}
} }
}
} }

View file

@ -24,13 +24,12 @@ object ApiModule {
@Provides @Provides
fun provideClient(): OkHttpClient { fun provideClient(): OkHttpClient {
return OkHttpClient.Builder() return OkHttpClient.Builder().build()
.build()
} }
/** /**
* Using [Lazy] here is a trick I picked up from Zac Sweers, which he explained in more * Using [Lazy] here is a trick I picked up from Zac Sweers, which he explained in more detail
* detail here: https://www.zacsweers.dev/dagger-party-tricks-deferred-okhttp-init/ * here: https://www.zacsweers.dev/dagger-party-tricks-deferred-okhttp-init/
*/ */
@Provides @Provides
fun provideRetrofit( fun provideRetrofit(

View file

@ -6,6 +6,4 @@ import javax.inject.Qualifier
* Qualifier for a string value that needs to be provided to the [ApiModule.provideRetrofit] method * Qualifier for a string value that needs to be provided to the [ApiModule.provideRetrofit] method
* as the base URL of our API. * as the base URL of our API.
*/ */
@Qualifier @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrlQualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class BaseUrlQualifier

View file

@ -32,10 +32,7 @@ object DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun providesLobstersDatabase( fun providesLobstersDatabase(sqlDriver: SqlDriver, tagsAdapter: TagsAdapter): LobstersDatabase {
sqlDriver: SqlDriver,
tagsAdapter: TagsAdapter
): LobstersDatabase {
return LobstersDatabase( return LobstersDatabase(
sqlDriver, sqlDriver,
SavedPost.Adapter(tagsAdapter), SavedPost.Adapter(tagsAdapter),

View file

@ -13,7 +13,6 @@ object MoshiModule {
@Provides @Provides
@Reusable @Reusable
fun provideMoshi(): Moshi { fun provideMoshi(): Moshi {
return Moshi.Builder() return Moshi.Builder().build()
.build()
} }
} }

View file

@ -62,8 +62,7 @@ fun LobstersApp() {
newestPostsListState.animateScrollToItem(index) newestPostsListState.animateScrollToItem(index)
} }
} }
else -> { else -> {}
}
} }
} }

View file

@ -36,9 +36,10 @@ fun LobstersTopAppBar(
IconResource( IconResource(
resourceId = R.drawable.ic_sort_24px, resourceId = R.drawable.ic_sort_24px,
contentDescription = Strings.ChangeSortingOrder.get(), contentDescription = Strings.ChangeSortingOrder.get(),
modifier = Modifier modifier =
.padding(horizontal = 8.dp, vertical = 8.dp) Modifier.padding(horizontal = 8.dp, vertical = 8.dp).clickable {
.clickable { scope.launch { toggleSortingOrder() } }, scope.launch { toggleSortingOrder() }
},
) )
} }
} }

View file

@ -18,9 +18,7 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
CompositionLocalProvider(LocalUrlLauncher provides urlLauncher) { CompositionLocalProvider(LocalUrlLauncher provides urlLauncher) {
LobstersTheme { LobstersTheme { LobstersApp() }
LobstersApp()
}
} }
} }
} }

View file

@ -4,9 +4,7 @@ import androidx.annotation.DrawableRes
import dev.msfjarvis.lobsters.R import dev.msfjarvis.lobsters.R
import dev.msfjarvis.lobsters.utils.Strings import dev.msfjarvis.lobsters.utils.Strings
/** /** Destinations for navigation within the app. */
* Destinations for navigation within the app.
*/
enum class Destination( enum class Destination(
val route: String, val route: String,
val labelRes: Strings, val labelRes: Strings,

View file

@ -14,24 +14,24 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
import java.time.Month import java.time.Month
import java.util.Locale
import java.time.format.TextStyle as JTextStyle import java.time.format.TextStyle as JTextStyle
import java.util.Locale
@Composable @Composable
fun MonthHeader(month: Month) { fun MonthHeader(month: Month) {
Box( Box(
Modifier Modifier.fillMaxWidth()
.fillMaxWidth()
.background(MaterialTheme.colors.secondary) .background(MaterialTheme.colors.secondary)
.wrapContentHeight() .wrapContentHeight()
.padding(4.dp) .padding(4.dp)
) { ) {
Text( Text(
text = month.getDisplayName(JTextStyle.FULL, Locale.getDefault()), text = month.getDisplayName(JTextStyle.FULL, Locale.getDefault()),
style = MaterialTheme.typography.h5.copy( style =
color = MaterialTheme.colors.onSecondary, MaterialTheme.typography.h5.copy(
textAlign = TextAlign.Center, color = MaterialTheme.colors.onSecondary,
), textAlign = TextAlign.Center,
),
modifier = Modifier.padding(horizontal = 12.dp), modifier = Modifier.padding(horizontal = 12.dp),
) )
} }
@ -40,7 +40,5 @@ fun MonthHeader(month: Month) {
@Preview @Preview
@Composable @Composable
fun MonthHeaderPreview() { fun MonthHeaderPreview() {
LobstersTheme { LobstersTheme { MonthHeader(month = Month.JULY) }
MonthHeader(month = Month.JULY)
}
} }

View file

@ -39,16 +39,17 @@ import dev.msfjarvis.lobsters.util.IconResource
import dev.msfjarvis.lobsters.utils.Strings import dev.msfjarvis.lobsters.utils.Strings
import dev.msfjarvis.lobsters.utils.get import dev.msfjarvis.lobsters.utils.get
val TEST_POST = SavedPost( val TEST_POST =
shortId = "zqyydb", SavedPost(
title = "k2k20 hackathon report: Bob Beck on LibreSSL progress", shortId = "zqyydb",
url = "https://undeadly.org/cgi?action=article;sid=20200921105847", title = "k2k20 hackathon report: Bob Beck on LibreSSL progress",
createdAt = "2020-09-21T07:11:14.000-05:00", url = "https://undeadly.org/cgi?action=article;sid=20200921105847",
commentsUrl = "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on", createdAt = "2020-09-21T07:11:14.000-05:00",
submitterName = "Vigdis", commentsUrl = "https://lobste.rs/s/zqyydb/k2k20_hackathon_report_bob_beck_on",
submitterAvatarUrl = "/404.html", submitterName = "Vigdis",
tags = listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"), submitterAvatarUrl = "/404.html",
) tags = listOf("openbsd", "linux", "containers", "hack the planet", "no thanks"),
)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@ -61,22 +62,17 @@ fun LobstersItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Surface( Surface(
modifier = Modifier modifier = Modifier.clickable { viewPost.invoke() }.then(modifier),
.clickable { viewPost.invoke() }
.then(modifier),
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
.padding(horizontal = 12.dp, vertical = 4.dp),
) { ) {
PostTitle( PostTitle(
title = post.title, title = post.title,
modifier = Modifier modifier = Modifier.padding(bottom = 4.dp),
.padding(bottom = 4.dp),
) )
Row( Row(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
TagRow( TagRow(
@ -144,11 +140,8 @@ fun SubmitterAvatar(
data = "${LobstersApi.BASE_URL}/$avatarUrl", data = "${LobstersApi.BASE_URL}/$avatarUrl",
contentDescription = Strings.AvatarContentDescription.get(name), contentDescription = Strings.AvatarContentDescription.get(name),
fadeIn = true, fadeIn = true,
requestBuilder = { requestBuilder = { transformations(CircleCropTransformation()) },
transformations(CircleCropTransformation()) modifier = Modifier.requiredSize(24.dp),
},
modifier = Modifier
.requiredSize(24.dp),
) )
} }
@ -158,8 +151,7 @@ fun SubmitterNameText(
) { ) {
Text( Text(
text = Strings.SubmittedBy.get(name), text = Strings.SubmittedBy.get(name),
modifier = Modifier modifier = Modifier.padding(start = 4.dp),
.padding(start = 4.dp),
) )
} }
@ -172,15 +164,14 @@ fun SaveButton(
IconToggleButton( IconToggleButton(
checked = isSaved, checked = isSaved,
onCheckedChange = { onClick.invoke() }, onCheckedChange = { onClick.invoke() },
modifier = Modifier modifier = Modifier.requiredSize(32.dp).then(modifier),
.requiredSize(32.dp)
.then(modifier),
) { ) {
Crossfade(targetState = isSaved) { saved -> Crossfade(targetState = isSaved) { saved ->
IconResource( IconResource(
resourceId = if (saved) R.drawable.ic_favorite_24px else R.drawable.ic_favorite_border_24px, resourceId = if (saved) R.drawable.ic_favorite_24px else R.drawable.ic_favorite_border_24px,
tint = MaterialTheme.colors.secondary, tint = MaterialTheme.colors.secondary,
contentDescription = if (saved) Strings.RemoveFromSavedPosts.get() else Strings.AddToSavedPosts.get(), contentDescription =
if (saved) Strings.RemoveFromSavedPosts.get() else Strings.AddToSavedPosts.get(),
) )
} }
} }
@ -193,9 +184,7 @@ fun CommentsButton(
) { ) {
IconButton( IconButton(
onClick = onClick, onClick = onClick,
modifier = Modifier modifier = Modifier.requiredSize(32.dp).then(modifier),
.requiredSize(32.dp)
.then(modifier),
) { ) {
IconResource( IconResource(
resourceId = R.drawable.ic_insert_comment_24px, resourceId = R.drawable.ic_insert_comment_24px,
@ -220,9 +209,9 @@ fun TagRow(
tags.forEach { tag -> tags.forEach { tag ->
Text( Text(
text = tag, text = tag,
modifier = Modifier modifier =
.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp)) Modifier.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp))
.padding(vertical = 2.dp, horizontal = 6.dp), .padding(vertical = 2.dp, horizontal = 6.dp),
color = Color.DarkGray, color = Color.DarkGray,
) )
} }

View file

@ -17,9 +17,7 @@ import dev.msfjarvis.lobsters.model.LobstersPost
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
import dev.msfjarvis.lobsters.util.toDbModel import dev.msfjarvis.lobsters.util.toDbModel
/** /** Composable for rendering a list of [LobstersPost] fetched from the network. */
* Composable for rendering a list of [LobstersPost] fetched from the network.
*/
@Composable @Composable
fun NetworkPosts( fun NetworkPosts(
posts: LazyPagingItems<LobstersPost>, posts: LazyPagingItems<LobstersPost>,
@ -42,11 +40,7 @@ fun NetworkPosts(
}, },
) { ) {
if (posts.loadState.refresh == LoadState.Loading) { if (posts.loadState.refresh == LoadState.Loading) {
LazyColumn { LazyColumn { items(15) { LoadingLobstersItem() } }
items(15) {
LoadingLobstersItem()
}
}
} else { } else {
LazyColumn( LazyColumn(
state = listState, state = listState,
@ -54,8 +48,7 @@ fun NetworkPosts(
) { ) {
items(posts) { item -> items(posts) { item ->
if (item != null) { if (item != null) {
@Suppress("NAME_SHADOWING") @Suppress("NAME_SHADOWING") val item = item.toDbModel()
val item = item.toDbModel()
var isSaved by remember(item.shortId) { mutableStateOf(isPostSaved(item.shortId)) } var isSaved by remember(item.shortId) { mutableStateOf(isPostSaved(item.shortId)) }
LobstersItem( LobstersItem(

View file

@ -58,11 +58,8 @@ fun SavedPosts(
) { ) {
val grouped = posts.groupBy { it.createdAt.asZonedDateTime().month } val grouped = posts.groupBy { it.createdAt.asZonedDateTime().month }
grouped.forEach { (month, posts) -> grouped.forEach { (month, posts) ->
stickyHeader { stickyHeader { MonthHeader(month = month) }
MonthHeader(month = month) @Suppress("NAME_SHADOWING") val posts = if (sortOrder) posts.reversed() else posts
}
@Suppress("NAME_SHADOWING")
val posts = if (sortOrder) posts.reversed() else posts
items(posts) { item -> items(posts) { item ->
LobstersItem( LobstersItem(
post = item, post = item,

View file

@ -34,13 +34,15 @@ fun LoadingLobstersItem() {
val alpha by infiniteTransition.animateFloat( val alpha by infiniteTransition.animateFloat(
initialValue = 0.2f, initialValue = 0.2f,
targetValue = 1f, targetValue = 1f,
animationSpec = infiniteRepeatable( animationSpec =
animation = keyframes { infiniteRepeatable(
durationMillis = 1000 animation =
0.7f at 500 keyframes {
}, durationMillis = 1000
repeatMode = RepeatMode.Reverse 0.7f at 500
) },
repeatMode = RepeatMode.Reverse
)
) )
val color = Color.LightGray.copy(alpha = alpha) val color = Color.LightGray.copy(alpha = alpha)
Surface( Surface(
@ -54,29 +56,22 @@ fun LoadingLobstersItem() {
verticalArrangement = Arrangement.SpaceEvenly, verticalArrangement = Arrangement.SpaceEvenly,
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxWidth().requiredHeight(12.dp).background(color).padding(8.dp),
.fillMaxWidth()
.requiredHeight(12.dp)
.background(color)
.padding(8.dp),
) )
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier modifier = Modifier.absoluteOffset(y = 12.dp),
.absoluteOffset(y = 12.dp),
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier.requiredSize(30.dp).background(color = color, shape = CircleShape),
.requiredSize(30.dp)
.background(color = color, shape = CircleShape),
) )
Box( Box(
modifier = Modifier modifier =
.requiredHeight(12.dp) Modifier.requiredHeight(12.dp)
.requiredWidth(40.dp) .requiredWidth(40.dp)
.absoluteOffset(x = 12.dp) .absoluteOffset(x = 12.dp)
.background(color), .background(color),
) )
} }
} }
@ -87,9 +82,5 @@ fun LoadingLobstersItem() {
@Preview @Preview
@Composable @Composable
fun ShimmerListPreview() { fun ShimmerListPreview() {
LazyColumn { LazyColumn { items(10) { LoadingLobstersItem() } }
items(10) {
LoadingLobstersItem()
}
}
} }

View file

@ -9,27 +9,29 @@ import androidx.compose.ui.graphics.Color
val titleColor = Color(0xFF7395D9) val titleColor = Color(0xFF7395D9)
val lightColors = lightColors( val lightColors =
primary = Color.White, lightColors(
secondary = Color(0xFF6C0000), primary = Color.White,
background = Color.White, secondary = Color(0xFF6C0000),
surface = Color.White, background = Color.White,
onPrimary = Color.DarkGray, surface = Color.White,
onSecondary = Color.White, onPrimary = Color.DarkGray,
onBackground = Color.White, onSecondary = Color.White,
onSurface = Color.White, onBackground = Color.White,
) onSurface = Color.White,
)
val darkColors = darkColors( val darkColors =
primary = Color.White, darkColors(
secondary = Color(0xFFD2362D), primary = Color.White,
background = Color.Black, secondary = Color(0xFFD2362D),
surface = Color.Black, background = Color.Black,
onPrimary = Color.Black, surface = Color.Black,
onSecondary = Color.White, onPrimary = Color.Black,
onBackground = Color.White, onSecondary = Color.White,
onSurface = Color.White, onBackground = Color.White,
) onSurface = Color.White,
)
@Composable @Composable
fun LobstersTheme(children: @Composable () -> Unit) { fun LobstersTheme(children: @Composable () -> Unit) {

View file

@ -20,27 +20,38 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@HiltViewModel @HiltViewModel
class LobstersViewModel @Inject constructor( class LobstersViewModel
@Inject
constructor(
private val lobstersRepository: LobstersRepository, private val lobstersRepository: LobstersRepository,
private val clawPreferences: ClawPreferences, private val clawPreferences: ClawPreferences,
) : ViewModel() { ) : ViewModel() {
private val _savedPosts = MutableStateFlow<List<SavedPost>>(emptyList()) private val _savedPosts = MutableStateFlow<List<SavedPost>>(emptyList())
val savedPosts = _savedPosts.asStateFlow() val savedPosts = _savedPosts.asStateFlow()
val hottestPosts = Pager(PagingConfig(25)) { val hottestPosts =
HottestPostsPagingSource(lobstersRepository).also { hottestPostsPagingSource = it } Pager(PagingConfig(25)) {
}.flow.cachedIn(viewModelScope) HottestPostsPagingSource(lobstersRepository).also { hottestPostsPagingSource = it }
val newestPosts = Pager(PagingConfig(25)) { }
NewestPostsPagingSource(lobstersRepository).also { newestPostsPagingSource = it } .flow
}.flow.cachedIn(viewModelScope) .cachedIn(viewModelScope)
val newestPosts =
Pager(PagingConfig(25)) {
NewestPostsPagingSource(lobstersRepository).also { newestPostsPagingSource = it }
}
.flow
.cachedIn(viewModelScope)
private var hottestPostsPagingSource: HottestPostsPagingSource? = null private var hottestPostsPagingSource: HottestPostsPagingSource? = null
private var newestPostsPagingSource: NewestPostsPagingSource? = null private var newestPostsPagingSource: NewestPostsPagingSource? = null
init { init {
lobstersRepository.isCacheReady.onEach { ready -> lobstersRepository
if (ready) { .isCacheReady
_savedPosts.value = lobstersRepository.getAllPostsFromCache() .onEach { ready ->
if (ready) {
_savedPosts.value = lobstersRepository.getAllPostsFromCache()
}
} }
}.launchIn(viewModelScope) .launchIn(viewModelScope)
} }
fun getSortOrder(): Flow<Boolean> { fun getSortOrder(): Flow<Boolean> {

View file

@ -3,9 +3,7 @@ package dev.msfjarvis.lobsters.util
import dev.msfjarvis.lobsters.data.local.SavedPost import dev.msfjarvis.lobsters.data.local.SavedPost
import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.model.LobstersPost
/** /** Convert a [LobstersPost] object returned by the API into a [SavedPost] for persistence. */
* Convert a [LobstersPost] object returned by the API into a [SavedPost] for persistence.
*/
fun LobstersPost.toDbModel(): SavedPost { fun LobstersPost.toDbModel(): SavedPost {
return SavedPost( return SavedPost(
shortId = shortId, shortId = shortId,

View file

@ -1,6 +1,4 @@
plugins { plugins { `lobsters-plugin` }
`lobsters-plugin`
}
subprojects { subprojects {
configurations.configureEach { configurations.configureEach {

View file

@ -1,15 +1,11 @@
plugins { plugins { `kotlin-dsl` }
`kotlin-dsl`
}
repositories { repositories {
google() google()
gradlePluginPortal() gradlePluginPortal()
} }
kotlinDslPluginOptions { kotlinDslPluginOptions { experimentalWarning.set(false) }
experimentalWarning.set(false)
}
gradlePlugin { gradlePlugin {
plugins { plugins {

View file

@ -1,6 +1,4 @@
plugins { plugins { `kotlin-dsl` }
`kotlin-dsl`
}
repositories { repositories {
mavenCentral() mavenCentral()
@ -8,9 +6,7 @@ repositories {
gradlePluginPortal() gradlePluginPortal()
} }
kotlinDslPluginOptions { kotlinDslPluginOptions { experimentalWarning.set(false) }
experimentalWarning.set(false)
}
// force compilation of Dependencies.kt so it can be referenced in buildSrc/build.gradle.kts // force compilation of Dependencies.kt so it can be referenced in buildSrc/build.gradle.kts
sourceSets.main { sourceSets.main {

View file

@ -18,14 +18,12 @@ import org.gradle.kotlin.dsl.withType
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
/** /**
* Configure root project. * Configure root project. Note that classpath dependencies still need to be defined in the
* Note that classpath dependencies still need to be defined in the `buildscript` block in the top-level build.gradle.kts file. * `buildscript` block in the top-level build.gradle.kts file.
*/ */
internal fun Project.configureForRootProject() { internal fun Project.configureForRootProject() {
// register task for cleaning the build directory in the root project // register task for cleaning the build directory in the root project
tasks.register<Delete>("clean") { tasks.register<Delete>("clean") { delete(rootProject.buildDir) }
delete(rootProject.buildDir)
}
tasks.withType<Wrapper> { tasks.withType<Wrapper> {
gradleVersion = "7.0-rc-1" gradleVersion = "7.0-rc-1"
distributionType = Wrapper.DistributionType.ALL distributionType = Wrapper.DistributionType.ALL
@ -33,18 +31,12 @@ internal fun Project.configureForRootProject() {
} }
} }
/** /** Configure all projects including the root project */
* Configure all projects including the root project
*/
internal fun Project.configureForAllProjects() { internal fun Project.configureForAllProjects() {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
jcenter { jcenter { content { includeGroup("org.jetbrains.compose.*") } }
content {
includeGroup("org.jetbrains.compose.*")
}
}
maven("https://dl.bintray.com/kotlin/kotlinx") { maven("https://dl.bintray.com/kotlin/kotlinx") {
name = "KotlinX Bintray" name = "KotlinX Bintray"
content { content {
@ -62,25 +54,23 @@ internal fun Project.configureForAllProjects() {
} }
tasks.withType<Test> { tasks.withType<Test> {
maxParallelForks = Runtime.getRuntime().availableProcessors() * 2 maxParallelForks = Runtime.getRuntime().availableProcessors() * 2
testLogging { testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
} }
} }
/** /** Apply configuration options for Android Application projects. */
* Apply configuration options for Android Application projects.
*/
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
internal fun BaseAppModuleExtension.configureAndroidApplicationOptions(project: Project) { internal fun BaseAppModuleExtension.configureAndroidApplicationOptions(project: Project) {
val minifySwitch = val minifySwitch =
project.providers.environmentVariable("DISABLE_MINIFY").forUseAtConfigurationTime() project.providers.environmentVariable("DISABLE_MINIFY").forUseAtConfigurationTime()
project.tasks.withType<KotlinCompile> { project.tasks.withType<KotlinCompile> {
kotlinOptions { kotlinOptions {
freeCompilerArgs = freeCompilerArgs + listOf( freeCompilerArgs =
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", freeCompilerArgs +
"-Xopt-in=androidx.compose.material.ExperimentalMaterialApi" listOf(
) "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=androidx.compose.material.ExperimentalMaterialApi"
)
} }
} }
adbOptions.installOptions("--user 0") adbOptions.installOptions("--user 0")
@ -97,9 +87,7 @@ internal fun BaseAppModuleExtension.configureAndroidApplicationOptions(project:
} }
} }
/** /** Apply baseline configurations for all Android projects (Application and Library). */
* Apply baseline configurations for all Android projects (Application and Library).
*/
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
internal fun TestedExtension.configureCommonAndroidOptions() { internal fun TestedExtension.configureCommonAndroidOptions() {
compileSdkVersion(30) compileSdkVersion(30)

View file

@ -9,7 +9,8 @@ import org.gradle.kotlin.dsl.withType
/** /**
* A plugin that enables Java 8 desugaring for consuming new Java language APIs. * A plugin that enables Java 8 desugaring for consuming new Java language APIs.
* *
* Apply this plugin to the build.gradle.kts file in Android Application or Android Library projects: * Apply this plugin to the build.gradle.kts file in Android Application or Android Library
* projects:
* ``` * ```
* plugins { * plugins {
* `core-library-desugaring` * `core-library-desugaring`

View file

@ -2,9 +2,7 @@ import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.plugin.KaptExtension import org.jetbrains.kotlin.gradle.plugin.KaptExtension
/** /** Apply default kapt configs to the [Project]. */
* Apply default kapt configs to the [Project].
*/
internal fun Project.configureKapt() { internal fun Project.configureKapt() {
extensions.configure<KaptExtension> { extensions.configure<KaptExtension> {
javacOptions { javacOptions {
@ -17,15 +15,14 @@ internal fun Project.configureKapt() {
} }
} }
// disable kapt tasks for unit tests // disable kapt tasks for unit tests
tasks.matching { tasks
it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") .matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
}.configureEach { enabled = false } .configureEach { enabled = false }
} }
} }
private val Project.hasDaggerCompilerDependency: Boolean private val Project.hasDaggerCompilerDependency: Boolean
get() = configurations.any { get() =
it.dependencies.any { dependency -> configurations.any {
dependency.name == "dagger-compiler" it.dependencies.any { dependency -> dependency.name == "dagger-compiler" }
} }
}

View file

@ -3,7 +3,5 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
internal val additionalCompilerArgs = listOf( internal val additionalCompilerArgs =
"-Xopt-in=kotlin.RequiresOptIn", listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xskip-prerelease-check")
"-Xskip-prerelease-check"
)

View file

@ -29,8 +29,7 @@ class LobstersPlugin : Plugin<Project> {
project.plugins.all { project.plugins.all {
when (this) { when (this) {
is JavaPlugin, is JavaPlugin, is JavaLibraryPlugin -> {
is JavaLibraryPlugin -> {
project.tasks.withType<JavaCompile> { project.tasks.withType<JavaCompile> {
options.compilerArgs.add("-Xlint:unchecked") options.compilerArgs.add("-Xlint:unchecked")
options.isDeprecation = true options.isDeprecation = true
@ -42,7 +41,9 @@ class LobstersPlugin : Plugin<Project> {
is AppPlugin -> { is AppPlugin -> {
project.extensions.getByType<TestedExtension>().configureCommonAndroidOptions() project.extensions.getByType<TestedExtension>().configureCommonAndroidOptions()
project.extensions.getByType<BaseAppModuleExtension>().configureBuildSigning(project) project.extensions.getByType<BaseAppModuleExtension>().configureBuildSigning(project)
project.extensions.getByType<BaseAppModuleExtension>() project
.extensions
.getByType<BaseAppModuleExtension>()
.configureAndroidApplicationOptions(project) .configureAndroidApplicationOptions(project)
} }
is Kapt3GradleSubplugin -> { is Kapt3GradleSubplugin -> {
@ -56,4 +57,5 @@ class LobstersPlugin : Plugin<Project> {
} }
} }
private val Project.isRoot get() = this == this.rootProject private val Project.isRoot
get() = this == this.rootProject

View file

@ -9,9 +9,7 @@ import org.gradle.api.Project
private const val KEYSTORE_CONFIG_PATH = "keystore.properties" private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
/** /** Configure signing for all build types. */
* Configure signing for all build types.
*/
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
internal fun BaseAppModuleExtension.configureBuildSigning(project: Project) { internal fun BaseAppModuleExtension.configureBuildSigning(project: Project) {
with(project) { with(project) {

View file

@ -3,7 +3,6 @@
* SPDX-License-Identifier: GPL-3.0-only * SPDX-License-Identifier: GPL-3.0-only
*/ */
import com.android.build.gradle.internal.plugins.AppPlugin import com.android.build.gradle.internal.plugins.AppPlugin
import com.github.zafarkhaja.semver.Version import com.github.zafarkhaja.semver.Version
import java.io.OutputStream import java.io.OutputStream
@ -14,34 +13,26 @@ import org.gradle.api.Project
private const val VERSIONING_PROP_FILE = "version.properties" private const val VERSIONING_PROP_FILE = "version.properties"
private const val VERSIONING_PROP_VERSION_NAME = "versioning-plugin.versionName" private const val VERSIONING_PROP_VERSION_NAME = "versioning-plugin.versionName"
private const val VERSIONING_PROP_VERSION_CODE = "versioning-plugin.versionCode" private const val VERSIONING_PROP_VERSION_CODE = "versioning-plugin.versionCode"
private const val VERSIONING_PROP_COMMENT = """ private const val VERSIONING_PROP_COMMENT =
"""
This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY. This file was automatically generated by 'versioning-plugin'. DO NOT EDIT MANUALLY.
""" """
/** /**
* A Gradle [Plugin] that takes a [Project] with the [AppPlugin] applied and dynamically sets the * A Gradle [Plugin] that takes a [Project] with the [AppPlugin] applied and dynamically sets the
* versionCode and versionName properties based on values read from a [VERSIONING_PROP_FILE] file in * versionCode and versionName properties based on values read from a [VERSIONING_PROP_FILE] file in
* the [Project.getBuildDir] directory. It also adds Gradle tasks to bump the major, minor, and patch * the [Project.getBuildDir] directory. It also adds Gradle tasks to bump the major, minor, and
* versions along with one to prepare the next snapshot. * patch versions along with one to prepare the next snapshot.
*/ */
@Suppress( @Suppress("UnstableApiUsage", "NAME_SHADOWING")
"UnstableApiUsage",
"NAME_SHADOWING"
)
class VersioningPlugin : Plugin<Project> { class VersioningPlugin : Plugin<Project> {
/** /** Generate the Android 'versionCode' property */
* Generate the Android 'versionCode' property
*/
private fun Version.androidCode(): Int { private fun Version.androidCode(): Int {
return majorVersion * 1_00_00 + return majorVersion * 1_00_00 + minorVersion * 1_00 + patchVersion
minorVersion * 1_00 +
patchVersion
} }
/** /** Write an Android-specific variant of [this] to [stream] */
* Write an Android-specific variant of [this] to [stream]
*/
private fun Version.writeForAndroid(stream: OutputStream) { private fun Version.writeForAndroid(stream: OutputStream) {
val newVersionCode = androidCode() val newVersionCode = androidCode()
val props = Properties() val props = Properties()
@ -50,27 +41,27 @@ class VersioningPlugin : Plugin<Project> {
props.store(stream, VERSIONING_PROP_COMMENT) props.store(stream, VERSIONING_PROP_COMMENT)
} }
/** /** Returns the same [Version], but with build metadata stripped. */
* Returns the same [Version], but with build metadata stripped.
*/
private fun Version.clearPreRelease(): Version { private fun Version.clearPreRelease(): Version {
return Version.forIntegers(majorVersion, minorVersion, patchVersion) return Version.forIntegers(majorVersion, minorVersion, patchVersion)
} }
override fun apply(project: Project) { override fun apply(project: Project) {
with(project) { with(project) {
val appPlugin = requireNotNull(plugins.findPlugin(AppPlugin::class.java)) { val appPlugin =
"Plugin 'com.android.application' must be applied to use this plugin" requireNotNull(plugins.findPlugin(AppPlugin::class.java)) {
} "Plugin 'com.android.application' must be applied to use this plugin"
}
val propFile = layout.projectDirectory.file(VERSIONING_PROP_FILE) val propFile = layout.projectDirectory.file(VERSIONING_PROP_FILE)
require(propFile.asFile.exists()) { require(propFile.asFile.exists()) {
"A 'version.properties' file must exist in the project subdirectory to use this plugin" "A 'version.properties' file must exist in the project subdirectory to use this plugin"
} }
val contents = providers.fileContents(propFile).asText.forUseAtConfigurationTime() val contents = providers.fileContents(propFile).asText.forUseAtConfigurationTime()
val versionProps = Properties().also { it.load(contents.get().byteInputStream()) } val versionProps = Properties().also { it.load(contents.get().byteInputStream()) }
val versionName = requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) { val versionName =
"version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property" requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
} "version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property"
}
val versionCode = val versionCode =
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE).toInt()) { requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_CODE).toInt()) {
"version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property" "version.properties must contain a '$VERSIONING_PROP_VERSION_CODE' property"
@ -80,32 +71,21 @@ class VersioningPlugin : Plugin<Project> {
afterEvaluate { afterEvaluate {
val version = Version.valueOf(versionName) val version = Version.valueOf(versionName)
tasks.register("clearPreRelease") { tasks.register("clearPreRelease") {
doLast { doLast { version.clearPreRelease().writeForAndroid(propFile.asFile.outputStream()) }
version.clearPreRelease()
.writeForAndroid(propFile.asFile.outputStream())
}
} }
tasks.register("bumpMajor") { tasks.register("bumpMajor") {
doLast { doLast { version.incrementMajorVersion().writeForAndroid(propFile.asFile.outputStream()) }
version.incrementMajorVersion()
.writeForAndroid(propFile.asFile.outputStream())
}
} }
tasks.register("bumpMinor") { tasks.register("bumpMinor") {
doLast { doLast { version.incrementMinorVersion().writeForAndroid(propFile.asFile.outputStream()) }
version.incrementMinorVersion()
.writeForAndroid(propFile.asFile.outputStream())
}
} }
tasks.register("bumpPatch") { tasks.register("bumpPatch") {
doLast { doLast { version.incrementPatchVersion().writeForAndroid(propFile.asFile.outputStream()) }
version.incrementPatchVersion()
.writeForAndroid(propFile.asFile.outputStream())
}
} }
tasks.register("bumpSnapshot") { tasks.register("bumpSnapshot") {
doLast { doLast {
version.incrementMinorVersion() version
.incrementMinorVersion()
.setPreReleaseVersion("SNAPSHOT") .setPreReleaseVersion("SNAPSHOT")
.writeForAndroid(propFile.asFile.outputStream()) .writeForAndroid(propFile.asFile.outputStream())
} }

View file

@ -5,9 +5,7 @@ plugins {
`lobsters-plugin` `lobsters-plugin`
} }
repositories { repositories { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
// workaround for https://youtrack.jetbrains.com/issue/KT-43944 // workaround for https://youtrack.jetbrains.com/issue/KT-43944
android { android {
@ -40,23 +38,11 @@ kotlin {
} }
} }
val jvmMain by getting { val jvmMain by getting { dependencies { implementation(compose.runtime) } }
dependencies {
implementation(compose.runtime)
}
}
val androidTest by getting { val androidTest by getting { dependencies { implementation(kotlin("test-junit")) } }
dependencies {
implementation(kotlin("test-junit"))
}
}
val jvmTest by getting { val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) } }
dependencies {
implementation(kotlin("test-junit"))
}
}
val commonTest by getting { val commonTest by getting {
dependencies { dependencies {
@ -68,9 +54,7 @@ kotlin {
} }
android { android {
buildFeatures { buildFeatures { androidResources = true }
androidResources = true
}
sourceSets { sourceSets {
named("main") { named("main") {

View file

@ -6,11 +6,12 @@ import androidx.browser.customtabs.CustomTabsIntent
actual class UrlLauncher(private val context: Context) { actual class UrlLauncher(private val context: Context) {
actual fun launch(url: String) { actual fun launch(url: String) {
val customTabsIntent = CustomTabsIntent.Builder() val customTabsIntent =
.setShareState(CustomTabsIntent.SHARE_STATE_ON) CustomTabsIntent.Builder()
.setShowTitle(true) .setShareState(CustomTabsIntent.SHARE_STATE_ON)
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_DARK) .setShowTitle(true)
.build() .setColorScheme(CustomTabsIntent.COLOR_SCHEME_DARK)
.build()
customTabsIntent.launchUrl(context, Uri.parse(url)) customTabsIntent.launchUrl(context, Uri.parse(url))
} }
} }

View file

@ -2,8 +2,6 @@ package dev.msfjarvis.lobsters.utils
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@Composable @Composable expect fun Strings.get(): String
expect fun Strings.get(): String
@Composable @Composable expect fun Strings.get(fmt: Any): String
expect fun Strings.get(fmt: Any): String

View file

@ -13,5 +13,4 @@ enum class Strings {
SavedPosts, SavedPosts,
SubmittedBy, SubmittedBy,
NewestPosts, NewestPosts,
;
} }

View file

@ -17,10 +17,11 @@ class SqlDelightQueriesTest {
fun setUp() { fun setUp() {
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
LobstersDatabase.Schema.create(driver) LobstersDatabase.Schema.create(driver)
val database = LobstersDatabase( val database =
driver, LobstersDatabase(
SavedPost.Adapter(TagsAdapter()), driver,
) SavedPost.Adapter(TagsAdapter()),
)
postQueries = database.savedPostQueries postQueries = database.savedPostQueries
} }
@ -128,21 +129,21 @@ class SqlDelightQueriesTest {
assertEquals(0, postsCount) assertEquals(0, postsCount)
} }
private fun createTestData(count: Int): ArrayList<SavedPost> { private fun createTestData(count: Int): ArrayList<SavedPost> {
val posts = arrayListOf<SavedPost>() val posts = arrayListOf<SavedPost>()
for (i in 1..count) { for (i in 1..count) {
val post = SavedPost( val post =
shortId = "test_id_$i", SavedPost(
createdAt = "0", shortId = "test_id_$i",
title = "test", createdAt = "0",
url = "test_url", title = "test",
commentsUrl = "test_comments_url", url = "test_url",
submitterName = "test_user_$i", commentsUrl = "test_comments_url",
submitterAvatarUrl = "test_avatar_url", submitterName = "test_user_$i",
tags = listOf(), submitterAvatarUrl = "test_avatar_url",
) tags = listOf(),
)
posts.add(post) posts.add(post)
} }

View file

@ -6,9 +6,7 @@ plugins {
`lobsters-plugin` `lobsters-plugin`
} }
repositories { repositories { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
dependencies { dependencies {
implementation(project(":api")) implementation(project(":api"))
@ -22,8 +20,4 @@ dependencies {
implementation(Dependencies.ThirdParty.Retrofit.moshi) implementation(Dependencies.ThirdParty.Retrofit.moshi)
} }
compose.desktop { compose.desktop { application { mainClass = "dev.msfjarvis.lobsters.ui.Main" } }
application {
mainClass = "dev.msfjarvis.lobsters.ui.Main"
}
}

View file

@ -8,12 +8,12 @@ import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.create import retrofit2.create
class ApiRepository { class ApiRepository {
private val moshi = Moshi.Builder() private val moshi = Moshi.Builder().build()
.build() private val retrofit =
private val retrofit = Retrofit.Builder() Retrofit.Builder()
.baseUrl(LobstersApi.BASE_URL) .baseUrl(LobstersApi.BASE_URL)
.addConverterFactory(MoshiConverterFactory.create(moshi)) .addConverterFactory(MoshiConverterFactory.create(moshi))
.build() .build()
private val api: LobstersApi = retrofit.create() private val api: LobstersApi = retrofit.create()
suspend fun loadPosts(pageNumber: Int): List<LobstersPost> { suspend fun loadPosts(pageNumber: Int): List<LobstersPost> {

View file

@ -29,16 +29,11 @@ fun LobstersItem(
post: SavedPost, post: SavedPost,
) { ) {
Surface( Surface(
modifier = Modifier modifier =
.fillMaxWidth() Modifier.fillMaxWidth().clickable { UrlLauncher.launch(post.url) }.wrapContentHeight(),
.clickable {
UrlLauncher.launch(post.url)
}
.wrapContentHeight(),
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier.padding(start = 12.dp, end = 24.dp),
.padding(start = 12.dp, end = 24.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
@ -49,26 +44,21 @@ fun LobstersItem(
text = post.title, text = post.title,
color = titleColor, color = titleColor,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier modifier = Modifier.padding(top = 4.dp),
.padding(top = 4.dp),
) )
TagRow( TagRow(
tags = post.tags, tags = post.tags,
modifier = Modifier modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, end = 16.dp),
.padding(top = 8.dp, bottom = 8.dp, end = 16.dp),
) )
Row { Row {
KamelImage( KamelImage(
resource = lazyImageResource(data = URI(post.submitterAvatarUrl)), resource = lazyImageResource(data = URI(post.submitterAvatarUrl)),
contentDescription = "${post.submitterName}'s avatar", contentDescription = "${post.submitterName}'s avatar",
modifier = Modifier modifier = Modifier.requiredWidth(30.dp).padding(4.dp),
.requiredWidth(30.dp)
.padding(4.dp),
) )
Text( Text(
text = "Submitted by ${post.submitterName}", text = "Submitted by ${post.submitterName}",
modifier = Modifier modifier = Modifier.padding(4.dp),
.padding(4.dp),
) )
} }
} }
@ -87,9 +77,9 @@ fun TagRow(
tags.forEach { tag -> tags.forEach { tag ->
Text( Text(
text = tag, text = tag,
modifier = Modifier modifier =
.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp)) Modifier.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp))
.padding(horizontal = 8.dp), .padding(horizontal = 8.dp),
color = Color.DarkGray, color = Color.DarkGray,
) )
} }

View file

@ -1,4 +1,5 @@
@file:JvmName("Main") @file:JvmName("Main")
package dev.msfjarvis.lobsters.ui package dev.msfjarvis.lobsters.ui
import androidx.compose.desktop.Window import androidx.compose.desktop.Window
@ -12,15 +13,15 @@ import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import dev.msfjarvis.lobsters.data.local.SavedPost
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import dev.msfjarvis.lobsters.data.ApiRepository import dev.msfjarvis.lobsters.data.ApiRepository
import dev.msfjarvis.lobsters.data.local.SavedPost
import dev.msfjarvis.lobsters.model.LobstersPost import dev.msfjarvis.lobsters.model.LobstersPost
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncher import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncher
@ -31,44 +32,37 @@ import kotlinx.coroutines.withContext
val repository = ApiRepository() val repository = ApiRepository()
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
fun main() = Window(title = "Claw for lobste.rs") { fun main() =
val urlLauncher = UrlLauncher() Window(title = "Claw for lobste.rs") {
val coroutineScope = rememberCoroutineScope() val urlLauncher = UrlLauncher()
var items by remember { mutableStateOf(emptyList<SavedPost>()) } val coroutineScope = rememberCoroutineScope()
coroutineScope.launch { var items by remember { mutableStateOf(emptyList<SavedPost>()) }
withContext(Dispatchers.IO) { coroutineScope.launch {
items = repository.loadPosts(0).map(::toDbModel) withContext(Dispatchers.IO) { items = repository.loadPosts(0).map(::toDbModel) }
} }
} LobstersTheme {
LobstersTheme {
Box(
modifier = Modifier.fillMaxSize(),
) {
val stateVertical = rememberScrollState(0)
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxSize(),
.fillMaxSize()
.verticalScroll(stateVertical),
) { ) {
if (items.isEmpty()) { val stateVertical = rememberScrollState(0)
Text("Loading...") Box(
} else { modifier = Modifier.fillMaxSize().verticalScroll(stateVertical),
CompositionLocalProvider(LocalUrlLauncher provides urlLauncher) { ) {
Column { if (items.isEmpty()) {
items.forEach { Text("Loading...")
LobstersItem(it) } else {
} CompositionLocalProvider(LocalUrlLauncher provides urlLauncher) {
Column { items.forEach { LobstersItem(it) } }
} }
} }
} }
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical),
)
} }
VerticalScrollbar(
modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(),
adapter = rememberScrollbarAdapter(stateVertical),
)
} }
} }
}
fun toDbModel(post: LobstersPost): SavedPost { fun toDbModel(post: LobstersPost): SavedPost {
return SavedPost( return SavedPost(

View file

@ -1,4 +1,5 @@
@file:Suppress("UNUSED") @file:Suppress("UNUSED")
package dev.msfjarvis.lobsters.ui package dev.msfjarvis.lobsters.ui
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
@ -9,33 +10,32 @@ import androidx.compose.ui.graphics.Color
val titleColor = Color(0xFF7395D9) val titleColor = Color(0xFF7395D9)
val lightColors = lightColors( val lightColors =
primary = Color.White, lightColors(
secondary = Color(0xFF6C0000), primary = Color.White,
background = Color.White, secondary = Color(0xFF6C0000),
surface = Color.White, background = Color.White,
onPrimary = Color.DarkGray, surface = Color.White,
onSecondary = Color.White, onPrimary = Color.DarkGray,
onBackground = Color.White, onSecondary = Color.White,
onSurface = Color.White, onBackground = Color.White,
) onSurface = Color.White,
)
val darkColors = darkColors( val darkColors =
primary = Color.White, darkColors(
secondary = Color(0xFF6C0000), primary = Color.White,
background = Color.Black, secondary = Color(0xFF6C0000),
surface = Color.Black, background = Color.Black,
onPrimary = Color.Black, surface = Color.Black,
onSecondary = Color.White, onPrimary = Color.Black,
onBackground = Color.White, onSecondary = Color.White,
onSurface = Color.White, onBackground = Color.White,
) onSurface = Color.White,
)
@Composable @Composable
fun LobstersTheme( fun LobstersTheme(useLightColors: Boolean = true, children: @Composable () -> Unit) {
useLightColors: Boolean = true,
children: @Composable () -> Unit
) {
MaterialTheme( MaterialTheme(
colors = if (useLightColors) lightColors else darkColors, colors = if (useLightColors) lightColors else darkColors,
content = children, content = children,

View file

@ -1,5 +1,7 @@
rootProject.name = "Claw" rootProject.name = "Claw"
include(":app", ":api", ":common", ":database", ":desktop") include(":app", ":api", ":common", ":database", ":desktop")
pluginManagement { pluginManagement {
repositories { repositories {
google() google()