mirror of
https://github.com/msfjarvis/compose-lobsters
synced 2025-08-17 08:37:03 +05:30
all: reformat with ktfmt google style
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
parent
8448910628
commit
db07a12be5
54 changed files with 496 additions and 656 deletions
|
@ -4,16 +4,12 @@ import dev.msfjarvis.lobsters.model.LobstersPost
|
|||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* Simple interface defining an API for lobste.rs
|
||||
*/
|
||||
/** Simple interface defining an API for lobste.rs */
|
||||
interface LobstersApi {
|
||||
|
||||
@GET("hottest.json")
|
||||
suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
|
||||
@GET("hottest.json") suspend fun getHottestPosts(@Query("page") page: Int): List<LobstersPost>
|
||||
|
||||
@GET("newest.json")
|
||||
suspend fun getNewestPosts(@Query("page") page: Int): List<LobstersPost>
|
||||
@GET("newest.json") suspend fun getNewestPosts(@Query("page") page: Int): List<LobstersPost>
|
||||
|
||||
companion object {
|
||||
const val BASE_URL = "https://lobste.rs"
|
||||
|
|
|
@ -5,8 +5,6 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class KeybaseSignature(
|
||||
@Json(name = "kb_username")
|
||||
val kbUsername: String,
|
||||
@Json(name = "sig_hash")
|
||||
val sigHash: String,
|
||||
@Json(name = "kb_username") val kbUsername: String,
|
||||
@Json(name = "sig_hash") val sigHash: String,
|
||||
)
|
||||
|
|
|
@ -5,22 +5,16 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
class LobstersPost(
|
||||
@Json(name = "short_id")
|
||||
val shortId: String,
|
||||
@Json(name = "short_id_url")
|
||||
val shortIdUrl: String,
|
||||
@Json(name = "created_at")
|
||||
val createdAt: String,
|
||||
@Json(name = "short_id") 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,
|
||||
@Json(name = "comment_count") val commentCount: Long,
|
||||
val description: String,
|
||||
@Json(name = "comments_url")
|
||||
val commentsUrl: String,
|
||||
@Json(name = "submitter_user")
|
||||
val submitterUser: Submitter,
|
||||
@Json(name = "comments_url") val commentsUrl: String,
|
||||
@Json(name = "submitter_user") val submitterUser: Submitter,
|
||||
val tags: List<String>,
|
||||
)
|
||||
|
|
|
@ -6,22 +6,14 @@ 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,
|
||||
@Json(name = "created_at") val createdAt: String,
|
||||
@Json(name = "is_admin") val isAdmin: Boolean,
|
||||
val about: String,
|
||||
@Json(name = "is_moderator")
|
||||
val isModerator: Boolean,
|
||||
@Json(name = "is_moderator") val isModerator: Boolean,
|
||||
val karma: Long = 0,
|
||||
@Json(name = "avatar_url")
|
||||
val avatarUrl: String,
|
||||
@Json(name = "invited_by_user")
|
||||
val invitedByUser: String,
|
||||
@Json(name = "github_username")
|
||||
val githubUsername: String? = null,
|
||||
@Json(name = "twitter_username")
|
||||
val twitterUsername: String? = null,
|
||||
@Json(name = "keybase_signatures")
|
||||
val keybaseSignatures: List<KeybaseSignature> = emptyList(),
|
||||
@Json(name = "avatar_url") val avatarUrl: String,
|
||||
@Json(name = "invited_by_user") val invitedByUser: String,
|
||||
@Json(name = "github_username") val githubUsername: String? = null,
|
||||
@Json(name = "twitter_username") val twitterUsername: String? = null,
|
||||
@Json(name = "keybase_signatures") val keybaseSignatures: List<KeybaseSignature> = emptyList(),
|
||||
)
|
||||
|
|
|
@ -22,11 +22,10 @@ class LobstersApiTest {
|
|||
companion object {
|
||||
private val webServer = MockWebServer()
|
||||
private val apiData = TestUtils.getJson("hottest.json")
|
||||
private val moshi = Moshi.Builder()
|
||||
.build()
|
||||
private val okHttp = OkHttpClient.Builder()
|
||||
.build()
|
||||
private val retrofit = Retrofit.Builder()
|
||||
private val moshi = Moshi.Builder().build()
|
||||
private val okHttp = OkHttpClient.Builder().build()
|
||||
private val retrofit =
|
||||
Retrofit.Builder()
|
||||
.client(okHttp)
|
||||
.baseUrl("http://localhost:8080/")
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
|
@ -37,7 +36,8 @@ class LobstersApiTest {
|
|||
@BeforeClass
|
||||
fun setUp() {
|
||||
webServer.start(8080)
|
||||
webServer.dispatcher = object : Dispatcher() {
|
||||
webServer.dispatcher =
|
||||
object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest): MockResponse {
|
||||
return MockResponse().setBody(apiData).setResponseCode(200)
|
||||
}
|
||||
|
@ -60,18 +60,14 @@ class LobstersApiTest {
|
|||
@Test
|
||||
fun `no moderator posts in test data`() = runBlocking {
|
||||
val posts = apiClient.getHottestPosts(1)
|
||||
val moderatorPosts = posts.asSequence()
|
||||
.filter { it.submitterUser.isModerator }
|
||||
.toSet()
|
||||
val moderatorPosts = posts.asSequence().filter { it.submitterUser.isModerator }.toSet()
|
||||
assertTrue(moderatorPosts.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `posts with no urls`() = runBlocking {
|
||||
val posts = apiClient.getHottestPosts(1)
|
||||
val commentsOnlyPosts = posts.asSequence()
|
||||
.filter { it.url.isEmpty() }
|
||||
.toSet()
|
||||
val commentsOnlyPosts = posts.asSequence().filter { it.url.isEmpty() }.toSet()
|
||||
assertEquals(2, commentsOnlyPosts.size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,7 @@ plugins {
|
|||
`core-library-desugaring`
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
}
|
||||
repositories { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
|
@ -22,7 +20,6 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
kapt(Dependencies.AndroidX.Hilt.daggerCompiler)
|
||||
implementation(project(":api"))
|
||||
implementation(project(":common"))
|
||||
|
|
|
@ -15,8 +15,7 @@ import org.junit.Test
|
|||
@Ignore("Shot is broken yet again")
|
||||
class LobstersTopBarTest : ScreenshotTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun showsRefreshIconWhenOnHottestPostsScreen_DarkTheme() {
|
||||
|
|
|
@ -14,7 +14,6 @@ import androidx.compose.ui.test.performClick
|
|||
import com.karumi.shot.ScreenshotTest
|
||||
import dev.msfjarvis.lobsters.ui.DarkTestTheme
|
||||
import dev.msfjarvis.lobsters.ui.main.LobstersBottomNav
|
||||
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
|
||||
import kotlin.test.Test
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
|
@ -22,8 +21,7 @@ import org.junit.Rule
|
|||
@Ignore("Shot is broken yet again")
|
||||
class LobstersBottomNavTest : ScreenshotTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun bottomNavIsRenderedCorrectlyOnScreen() {
|
||||
|
@ -59,8 +57,6 @@ class LobstersBottomNavTest : ScreenshotTest {
|
|||
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
|
||||
}
|
||||
|
||||
private fun selectNode(testTag: String) = composeTestRule
|
||||
.onNodeWithTag(testTag)
|
||||
.assertHasClickAction()
|
||||
.performClick()
|
||||
private fun selectNode(testTag: String) =
|
||||
composeTestRule.onNodeWithTag(testTag).assertHasClickAction().performClick()
|
||||
}
|
||||
|
|
|
@ -19,21 +19,16 @@ import org.junit.Rule
|
|||
|
||||
@Ignore("Shot is broken yet again")
|
||||
class HeaderTest : ScreenshotTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun headerDoesNotHaveATransparentBackground() {
|
||||
composeTestRule.setContent {
|
||||
DarkTestTheme {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(color = Color(0xffffff))
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
) {
|
||||
MonthHeader(month = Month.AUGUST)
|
||||
}
|
||||
modifier =
|
||||
Modifier.background(color = Color(0xffffff)).fillMaxWidth().wrapContentHeight(),
|
||||
) { MonthHeader(month = Month.AUGUST) }
|
||||
}
|
||||
}
|
||||
compareScreenshot(composeTestRule.onRoot().captureToImage().asAndroidBitmap())
|
||||
|
|
|
@ -15,8 +15,7 @@ import org.junit.Rule
|
|||
@Ignore("Shot is broken yet again")
|
||||
class LobstersItemTest : ScreenshotTest {
|
||||
|
||||
@get:Rule
|
||||
val composeTestRule = createComposeRule()
|
||||
@get:Rule val composeTestRule = createComposeRule()
|
||||
|
||||
@Test
|
||||
fun singlePost() {
|
||||
|
@ -24,9 +23,9 @@ class LobstersItemTest : ScreenshotTest {
|
|||
DarkTestTheme {
|
||||
LobstersItem(
|
||||
post = TEST_POST,
|
||||
viewPost = { /*TODO*/ },
|
||||
viewComments = { /*TODO*/ },
|
||||
toggleSave = { /*TODO*/ },
|
||||
viewPost = {},
|
||||
viewComments = {},
|
||||
toggleSave = {},
|
||||
isSaved = true,
|
||||
)
|
||||
}
|
||||
|
@ -42,9 +41,9 @@ class LobstersItemTest : ScreenshotTest {
|
|||
items(10) {
|
||||
LobstersItem(
|
||||
post = TEST_POST,
|
||||
viewPost = { /*TODO*/ },
|
||||
viewComments = { /*TODO*/ },
|
||||
toggleSave = { /*TODO*/ },
|
||||
viewPost = {},
|
||||
viewComments = {},
|
||||
toggleSave = {},
|
||||
isSaved = true,
|
||||
)
|
||||
}
|
||||
|
@ -62,9 +61,9 @@ class LobstersItemTest : ScreenshotTest {
|
|||
items(10) {
|
||||
LobstersItem(
|
||||
post = TEST_POST.copy(tags = listOf("openbsd", "linux")),
|
||||
viewPost = { /*TODO*/ },
|
||||
viewComments = { /*TODO*/ },
|
||||
toggleSave = { /*TODO*/ },
|
||||
viewPost = {},
|
||||
viewComments = {},
|
||||
toggleSave = {},
|
||||
isSaved = true,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,13 +25,11 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
private const val MAX_OFFSET = 400f
|
||||
private const val MIN_REFRESH_OFFSET = 250f
|
||||
private const val PERCENT_INDICATOR_PROGRESS_ON_DRAG = 0.85f
|
||||
private const val BASE_OFFSET = -48
|
||||
|
||||
|
||||
/**
|
||||
* A layout composable with [content].
|
||||
*
|
||||
|
@ -78,16 +76,19 @@ fun PullToRefresh(
|
|||
finishedListener = {
|
||||
indicatorOffset = 0f
|
||||
isFinishingRefresh = false
|
||||
})
|
||||
}
|
||||
)
|
||||
val offsetAnimation by animateFloatAsState(
|
||||
targetValue = if (isRefreshing || isFinishingRefresh) {
|
||||
targetValue =
|
||||
if (isRefreshing || isFinishingRefresh) {
|
||||
indicatorOffset - minRefreshOffset
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
)
|
||||
val resettingScrollOffsetAnimation by animateFloatAsState(
|
||||
targetValue = if (isResettingScroll) {
|
||||
targetValue =
|
||||
if (isResettingScroll) {
|
||||
scrollToReset
|
||||
} else {
|
||||
0f
|
||||
|
@ -97,7 +98,8 @@ fun PullToRefresh(
|
|||
indicatorOffset = 0f
|
||||
isResettingScroll = false
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
if (isResettingScroll) {
|
||||
indicatorOffset -= resettingScrollOffsetAnimation
|
||||
|
@ -110,7 +112,8 @@ fun PullToRefresh(
|
|||
isRefreshingInternal = true
|
||||
}
|
||||
|
||||
val nestedScrollConnection = object : NestedScrollConnection {
|
||||
val nestedScrollConnection =
|
||||
object : NestedScrollConnection {
|
||||
|
||||
override fun onPostScroll(
|
||||
consumed: Offset,
|
||||
|
@ -118,7 +121,8 @@ fun PullToRefresh(
|
|||
source: NestedScrollSource
|
||||
): Offset {
|
||||
if (!isRefreshing && source == NestedScrollSource.Drag) {
|
||||
val diff = if (indicatorOffset + available.y > maxOffset) {
|
||||
val diff =
|
||||
if (indicatorOffset + available.y > maxOffset) {
|
||||
available.y - (indicatorOffset + available.y - maxOffset)
|
||||
} else if (indicatorOffset + available.y < 0) {
|
||||
0f
|
||||
|
@ -131,13 +135,11 @@ fun PullToRefresh(
|
|||
return super.onPostScroll(consumed, available, source)
|
||||
}
|
||||
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
if (!isRefreshing && source == NestedScrollSource.Drag) {
|
||||
if (available.y < 0 && indicatorOffset > 0) {
|
||||
val diff = if (indicatorOffset + available.y < 0) {
|
||||
val diff =
|
||||
if (indicatorOffset + available.y < 0) {
|
||||
indicatorOffset = 0f
|
||||
indicatorOffset
|
||||
} else {
|
||||
|
@ -151,10 +153,7 @@ fun PullToRefresh(
|
|||
return super.onPreScroll(available, source)
|
||||
}
|
||||
|
||||
override suspend fun onPostFling(
|
||||
consumed: Velocity,
|
||||
available: Velocity
|
||||
): Velocity {
|
||||
override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
|
||||
if (!isRefreshing) {
|
||||
if (indicatorOffset > minRefreshOffset) {
|
||||
onRefresh()
|
||||
|
@ -169,39 +168,44 @@ fun PullToRefresh(
|
|||
}
|
||||
|
||||
Box(
|
||||
modifier = CombinedModifier(
|
||||
inner = Modifier
|
||||
.nestedScroll(nestedScrollConnection)
|
||||
.clip(RectangleShape),
|
||||
modifier =
|
||||
CombinedModifier(
|
||||
inner = Modifier.nestedScroll(nestedScrollConnection).clip(RectangleShape),
|
||||
outer = modifier
|
||||
)
|
||||
) {
|
||||
content()
|
||||
|
||||
val offsetPx = if (isRefreshing || isFinishingRefresh) {
|
||||
val offsetPx =
|
||||
if (isRefreshing || isFinishingRefresh) {
|
||||
offsetAnimation
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
val absoluteOffset = BASE_OFFSET.dp + with(LocalDensity.current) {
|
||||
val absoluteOffset =
|
||||
BASE_OFFSET.dp +
|
||||
with(LocalDensity.current) {
|
||||
val diffedOffset = indicatorOffset - offsetPx
|
||||
val calculated = calculateAbsoluteOffset(diffedOffset, MAX_OFFSET)
|
||||
calculated.toDp()
|
||||
}
|
||||
val progressFromOffset = with(LocalDensity.current) {
|
||||
val progressFromOffset =
|
||||
with(LocalDensity.current) {
|
||||
val coeff = MAX_OFFSET / (MAX_OFFSET - BASE_OFFSET)
|
||||
(indicatorOffset - BASE_OFFSET) / maxOffset * coeff
|
||||
}
|
||||
PullToRefreshProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
modifier =
|
||||
Modifier.fillMaxWidth()
|
||||
.absoluteOffset(y = absoluteOffset)
|
||||
.scale(scaleAnimation)
|
||||
.rotate(indicatorOffset / MAX_OFFSET * 180 + 110),
|
||||
progressColor = progressColor,
|
||||
backgroundColor = backgroundColor,
|
||||
progress = when {
|
||||
!isRefreshing && !isFinishingRefresh -> progressFromOffset * PERCENT_INDICATOR_PROGRESS_ON_DRAG
|
||||
progress =
|
||||
when {
|
||||
!isRefreshing && !isFinishingRefresh ->
|
||||
progressFromOffset * PERCENT_INDICATOR_PROGRESS_ON_DRAG
|
||||
else -> null
|
||||
},
|
||||
)
|
||||
|
|
|
@ -27,11 +27,9 @@ import androidx.compose.ui.platform.LocalDensity
|
|||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
|
||||
private val CircularIndicatorDiameter = 40.dp
|
||||
private const val strokeWidthPx = 2.5f
|
||||
|
||||
|
||||
@Composable
|
||||
internal fun PullToRefreshProgressIndicator(
|
||||
modifier: Modifier = Modifier,
|
||||
|
@ -39,22 +37,15 @@ internal fun PullToRefreshProgressIndicator(
|
|||
backgroundColor: Color,
|
||||
progress: Float? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Row(modifier = modifier, horizontalArrangement = Arrangement.Center) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.width(CircularIndicatorDiameter)
|
||||
.height(CircularIndicatorDiameter),
|
||||
modifier = Modifier.width(CircularIndicatorDiameter).height(CircularIndicatorDiameter),
|
||||
shape = CircleShape,
|
||||
elevation = 6.dp,
|
||||
backgroundColor = backgroundColor,
|
||||
) {
|
||||
val padding = Modifier.padding(8.dp)
|
||||
val strokeWidth = with(LocalDensity.current) {
|
||||
(strokeWidthPx * this.density).toDp()
|
||||
}
|
||||
val strokeWidth = with(LocalDensity.current) { (strokeWidthPx * this.density).toDp() }
|
||||
|
||||
if (progress == null) {
|
||||
CircularProgressIndicator(
|
||||
|
@ -74,7 +65,6 @@ internal fun PullToRefreshProgressIndicator(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun ProgressIndicatorWithArrow(
|
||||
progress: Float,
|
||||
|
@ -83,23 +73,22 @@ fun ProgressIndicatorWithArrow(
|
|||
strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth,
|
||||
) {
|
||||
|
||||
val strokeWidthPx = with(LocalDensity.current) {
|
||||
strokeWidth.toPx()
|
||||
}
|
||||
val strokeWidthPx = with(LocalDensity.current) { strokeWidth.toPx() }
|
||||
val arrowWidth = 2.5f * strokeWidthPx * (0.5f + progress * 0.5f)
|
||||
val stroke = Stroke(width = strokeWidthPx, cap = StrokeCap.Butt)
|
||||
val diameterOffset = stroke.width / 2
|
||||
|
||||
val arrowPath = Path().apply {
|
||||
val arrowPath =
|
||||
Path().apply {
|
||||
moveTo(0f, -arrowWidth)
|
||||
lineTo(arrowWidth, 0f)
|
||||
lineTo(0f, arrowWidth)
|
||||
close()
|
||||
}
|
||||
Box(modifier = modifier) {
|
||||
Canvas(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(), onDraw = {
|
||||
Canvas(
|
||||
modifier = Modifier.fillMaxWidth().fillMaxHeight(),
|
||||
onDraw = {
|
||||
val arcDimen = size.width - 2 * diameterOffset
|
||||
withTransform({
|
||||
translate(top = strokeWidthPx / 2, left = size.width / 2)
|
||||
|
@ -107,12 +96,7 @@ fun ProgressIndicatorWithArrow(
|
|||
degrees = progress * 360,
|
||||
pivot = Offset(x = 0f, y = size.height / 2 - diameterOffset)
|
||||
)
|
||||
}) {
|
||||
drawPath(
|
||||
path = arrowPath,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
}) { drawPath(path = arrowPath, color = color) }
|
||||
|
||||
drawArc(
|
||||
color = color,
|
||||
|
@ -123,7 +107,7 @@ fun ProgressIndicatorWithArrow(
|
|||
size = Size(arcDimen, arcDimen),
|
||||
style = stroke
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,4 @@ package dev.msfjarvis.lobsters
|
|||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class ClawApplication : Application()
|
||||
@HiltAndroidApp class ClawApplication : Application()
|
||||
|
|
|
@ -8,7 +8,9 @@ import javax.inject.Inject
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class ClawPreferences @Inject constructor(
|
||||
class ClawPreferences
|
||||
@Inject
|
||||
constructor(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) {
|
||||
private val sortKey = booleanPreferencesKey("post_sorting_order")
|
||||
|
@ -17,8 +19,6 @@ class ClawPreferences @Inject constructor(
|
|||
get() = dataStore.data.map { preferences -> preferences[sortKey] ?: false }
|
||||
|
||||
suspend fun toggleSortingOrder() {
|
||||
dataStore.edit { preferences ->
|
||||
preferences[sortKey] = (preferences[sortKey] ?: false).not()
|
||||
}
|
||||
dataStore.edit { preferences -> preferences[sortKey] = (preferences[sortKey] ?: false).not() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,8 @@ import androidx.paging.PagingState
|
|||
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
|
||||
class HottestPostsPagingSource constructor(
|
||||
class HottestPostsPagingSource
|
||||
constructor(
|
||||
private val lobstersRepository: LobstersRepository,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@ import androidx.paging.PagingState
|
|||
import dev.msfjarvis.lobsters.data.repo.LobstersRepository
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
|
||||
class NewestPostsPagingSource constructor(
|
||||
class NewestPostsPagingSource
|
||||
constructor(
|
||||
private val lobstersRepository: LobstersRepository,
|
||||
) : PagingSource<Int, LobstersPost>() {
|
||||
|
||||
|
|
|
@ -2,14 +2,15 @@ package dev.msfjarvis.lobsters.data.repo
|
|||
|
||||
import dev.msfjarvis.lobsters.data.api.LobstersApi
|
||||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.database.LobstersDatabase
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LobstersRepository constructor(
|
||||
class LobstersRepository
|
||||
constructor(
|
||||
private val lobstersApi: LobstersApi,
|
||||
private val lobstersDatabase: LobstersDatabase,
|
||||
) {
|
||||
|
@ -26,11 +27,13 @@ class LobstersRepository constructor(
|
|||
return savedPostsCache.values.toList()
|
||||
}
|
||||
|
||||
suspend fun fetchHottestPosts(page: Int): List<LobstersPost> = withContext(Dispatchers.IO) {
|
||||
suspend fun fetchHottestPosts(page: Int): List<LobstersPost> =
|
||||
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> =
|
||||
withContext(Dispatchers.IO) {
|
||||
return@withContext lobstersApi.getNewestPosts(page)
|
||||
}
|
||||
|
||||
|
@ -40,24 +43,25 @@ class LobstersRepository constructor(
|
|||
if (_isCacheReady.value) return
|
||||
val posts = getSavedPosts()
|
||||
|
||||
posts.forEach {
|
||||
savedPostsCache[it.shortId] = it
|
||||
}
|
||||
posts.forEach { savedPostsCache[it.shortId] = it }
|
||||
_isCacheReady.value = true
|
||||
}
|
||||
|
||||
private suspend fun getSavedPosts(): List<SavedPost> = withContext(Dispatchers.IO) {
|
||||
private suspend fun getSavedPosts(): List<SavedPost> =
|
||||
withContext(Dispatchers.IO) {
|
||||
return@withContext lobstersDatabase.savedPostQueries.selectAllPosts().executeAsList()
|
||||
}
|
||||
|
||||
suspend fun addPost(post: SavedPost) = withContext(Dispatchers.IO) {
|
||||
suspend fun addPost(post: SavedPost) =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!savedPostsCache.containsKey(post.shortId)) {
|
||||
savedPostsCache.putIfAbsent(post.shortId, post)
|
||||
lobstersDatabase.savedPostQueries.insertOrReplacePost(post)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removePost(post: SavedPost) = withContext(Dispatchers.IO) {
|
||||
suspend fun removePost(post: SavedPost) =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (savedPostsCache.containsKey(post.shortId)) {
|
||||
savedPostsCache.remove(post.shortId)
|
||||
lobstersDatabase.savedPostQueries.deletePost(post.shortId)
|
||||
|
|
|
@ -24,13 +24,12 @@ object ApiModule {
|
|||
|
||||
@Provides
|
||||
fun provideClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.build()
|
||||
return OkHttpClient.Builder().build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Using [Lazy] here is a trick I picked up from Zac Sweers, which he explained in more
|
||||
* detail here: https://www.zacsweers.dev/dagger-party-tricks-deferred-okhttp-init/
|
||||
* Using [Lazy] here is a trick I picked up from Zac Sweers, which he explained in more detail
|
||||
* here: https://www.zacsweers.dev/dagger-party-tricks-deferred-okhttp-init/
|
||||
*/
|
||||
@Provides
|
||||
fun provideRetrofit(
|
||||
|
|
|
@ -6,6 +6,4 @@ import javax.inject.Qualifier
|
|||
* Qualifier for a string value that needs to be provided to the [ApiModule.provideRetrofit] method
|
||||
* as the base URL of our API.
|
||||
*/
|
||||
@Qualifier
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class BaseUrlQualifier
|
||||
@Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BaseUrlQualifier
|
||||
|
|
|
@ -32,10 +32,7 @@ object DatabaseModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesLobstersDatabase(
|
||||
sqlDriver: SqlDriver,
|
||||
tagsAdapter: TagsAdapter
|
||||
): LobstersDatabase {
|
||||
fun providesLobstersDatabase(sqlDriver: SqlDriver, tagsAdapter: TagsAdapter): LobstersDatabase {
|
||||
return LobstersDatabase(
|
||||
sqlDriver,
|
||||
SavedPost.Adapter(tagsAdapter),
|
||||
|
|
|
@ -13,7 +13,6 @@ object MoshiModule {
|
|||
@Provides
|
||||
@Reusable
|
||||
fun provideMoshi(): Moshi {
|
||||
return Moshi.Builder()
|
||||
.build()
|
||||
return Moshi.Builder().build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,8 +62,7 @@ fun LobstersApp() {
|
|||
newestPostsListState.animateScrollToItem(index)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,9 +36,10 @@ fun LobstersTopAppBar(
|
|||
IconResource(
|
||||
resourceId = R.drawable.ic_sort_24px,
|
||||
contentDescription = Strings.ChangeSortingOrder.get(),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
.clickable { scope.launch { toggleSortingOrder() } },
|
||||
modifier =
|
||||
Modifier.padding(horizontal = 8.dp, vertical = 8.dp).clickable {
|
||||
scope.launch { toggleSortingOrder() }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,7 @@ class MainActivity : AppCompatActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
CompositionLocalProvider(LocalUrlLauncher provides urlLauncher) {
|
||||
LobstersTheme {
|
||||
LobstersApp()
|
||||
}
|
||||
LobstersTheme { LobstersApp() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,7 @@ import androidx.annotation.DrawableRes
|
|||
import dev.msfjarvis.lobsters.R
|
||||
import dev.msfjarvis.lobsters.utils.Strings
|
||||
|
||||
/**
|
||||
* Destinations for navigation within the app.
|
||||
*/
|
||||
/** Destinations for navigation within the app. */
|
||||
enum class Destination(
|
||||
val route: String,
|
||||
val labelRes: Strings,
|
||||
|
|
|
@ -14,21 +14,21 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.unit.dp
|
||||
import dev.msfjarvis.lobsters.ui.theme.LobstersTheme
|
||||
import java.time.Month
|
||||
import java.util.Locale
|
||||
import java.time.format.TextStyle as JTextStyle
|
||||
import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun MonthHeader(month: Month) {
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
Modifier.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.secondary)
|
||||
.wrapContentHeight()
|
||||
.padding(4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = month.getDisplayName(JTextStyle.FULL, Locale.getDefault()),
|
||||
style = MaterialTheme.typography.h5.copy(
|
||||
style =
|
||||
MaterialTheme.typography.h5.copy(
|
||||
color = MaterialTheme.colors.onSecondary,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
|
@ -40,7 +40,5 @@ fun MonthHeader(month: Month) {
|
|||
@Preview
|
||||
@Composable
|
||||
fun MonthHeaderPreview() {
|
||||
LobstersTheme {
|
||||
MonthHeader(month = Month.JULY)
|
||||
}
|
||||
LobstersTheme { MonthHeader(month = Month.JULY) }
|
||||
}
|
||||
|
|
|
@ -39,7 +39,8 @@ import dev.msfjarvis.lobsters.util.IconResource
|
|||
import dev.msfjarvis.lobsters.utils.Strings
|
||||
import dev.msfjarvis.lobsters.utils.get
|
||||
|
||||
val TEST_POST = SavedPost(
|
||||
val TEST_POST =
|
||||
SavedPost(
|
||||
shortId = "zqyydb",
|
||||
title = "k2k20 hackathon report: Bob Beck on LibreSSL progress",
|
||||
url = "https://undeadly.org/cgi?action=article;sid=20200921105847",
|
||||
|
@ -61,22 +62,17 @@ fun LobstersItem(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.clickable { viewPost.invoke() }
|
||||
.then(modifier),
|
||||
modifier = Modifier.clickable { viewPost.invoke() }.then(modifier),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
) {
|
||||
PostTitle(
|
||||
title = post.title,
|
||||
modifier = Modifier
|
||||
.padding(bottom = 4.dp),
|
||||
modifier = Modifier.padding(bottom = 4.dp),
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TagRow(
|
||||
|
@ -144,11 +140,8 @@ fun SubmitterAvatar(
|
|||
data = "${LobstersApi.BASE_URL}/$avatarUrl",
|
||||
contentDescription = Strings.AvatarContentDescription.get(name),
|
||||
fadeIn = true,
|
||||
requestBuilder = {
|
||||
transformations(CircleCropTransformation())
|
||||
},
|
||||
modifier = Modifier
|
||||
.requiredSize(24.dp),
|
||||
requestBuilder = { transformations(CircleCropTransformation()) },
|
||||
modifier = Modifier.requiredSize(24.dp),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -158,8 +151,7 @@ fun SubmitterNameText(
|
|||
) {
|
||||
Text(
|
||||
text = Strings.SubmittedBy.get(name),
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp),
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -172,15 +164,14 @@ fun SaveButton(
|
|||
IconToggleButton(
|
||||
checked = isSaved,
|
||||
onCheckedChange = { onClick.invoke() },
|
||||
modifier = Modifier
|
||||
.requiredSize(32.dp)
|
||||
.then(modifier),
|
||||
modifier = Modifier.requiredSize(32.dp).then(modifier),
|
||||
) {
|
||||
Crossfade(targetState = isSaved) { saved ->
|
||||
IconResource(
|
||||
resourceId = if (saved) R.drawable.ic_favorite_24px else R.drawable.ic_favorite_border_24px,
|
||||
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(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.requiredSize(32.dp)
|
||||
.then(modifier),
|
||||
modifier = Modifier.requiredSize(32.dp).then(modifier),
|
||||
) {
|
||||
IconResource(
|
||||
resourceId = R.drawable.ic_insert_comment_24px,
|
||||
|
@ -220,8 +209,8 @@ fun TagRow(
|
|||
tags.forEach { tag ->
|
||||
Text(
|
||||
text = tag,
|
||||
modifier = Modifier
|
||||
.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp))
|
||||
modifier =
|
||||
Modifier.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp))
|
||||
.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||
color = Color.DarkGray,
|
||||
)
|
||||
|
|
|
@ -17,9 +17,7 @@ import dev.msfjarvis.lobsters.model.LobstersPost
|
|||
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
||||
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
|
||||
fun NetworkPosts(
|
||||
posts: LazyPagingItems<LobstersPost>,
|
||||
|
@ -42,11 +40,7 @@ fun NetworkPosts(
|
|||
},
|
||||
) {
|
||||
if (posts.loadState.refresh == LoadState.Loading) {
|
||||
LazyColumn {
|
||||
items(15) {
|
||||
LoadingLobstersItem()
|
||||
}
|
||||
}
|
||||
LazyColumn { items(15) { LoadingLobstersItem() } }
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
|
@ -54,8 +48,7 @@ fun NetworkPosts(
|
|||
) {
|
||||
items(posts) { item ->
|
||||
if (item != null) {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val item = item.toDbModel()
|
||||
@Suppress("NAME_SHADOWING") val item = item.toDbModel()
|
||||
var isSaved by remember(item.shortId) { mutableStateOf(isPostSaved(item.shortId)) }
|
||||
|
||||
LobstersItem(
|
||||
|
|
|
@ -58,11 +58,8 @@ fun SavedPosts(
|
|||
) {
|
||||
val grouped = posts.groupBy { it.createdAt.asZonedDateTime().month }
|
||||
grouped.forEach { (month, posts) ->
|
||||
stickyHeader {
|
||||
MonthHeader(month = month)
|
||||
}
|
||||
@Suppress("NAME_SHADOWING")
|
||||
val posts = if (sortOrder) posts.reversed() else posts
|
||||
stickyHeader { MonthHeader(month = month) }
|
||||
@Suppress("NAME_SHADOWING") val posts = if (sortOrder) posts.reversed() else posts
|
||||
items(posts) { item ->
|
||||
LobstersItem(
|
||||
post = item,
|
||||
|
|
|
@ -34,8 +34,10 @@ fun LoadingLobstersItem() {
|
|||
val alpha by infiniteTransition.animateFloat(
|
||||
initialValue = 0.2f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = keyframes {
|
||||
animationSpec =
|
||||
infiniteRepeatable(
|
||||
animation =
|
||||
keyframes {
|
||||
durationMillis = 1000
|
||||
0.7f at 500
|
||||
},
|
||||
|
@ -54,26 +56,19 @@ fun LoadingLobstersItem() {
|
|||
verticalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.requiredHeight(12.dp)
|
||||
.background(color)
|
||||
.padding(8.dp),
|
||||
modifier = Modifier.fillMaxWidth().requiredHeight(12.dp).background(color).padding(8.dp),
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = Modifier
|
||||
.absoluteOffset(y = 12.dp),
|
||||
modifier = Modifier.absoluteOffset(y = 12.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredSize(30.dp)
|
||||
.background(color = color, shape = CircleShape),
|
||||
modifier = Modifier.requiredSize(30.dp).background(color = color, shape = CircleShape),
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.requiredHeight(12.dp)
|
||||
modifier =
|
||||
Modifier.requiredHeight(12.dp)
|
||||
.requiredWidth(40.dp)
|
||||
.absoluteOffset(x = 12.dp)
|
||||
.background(color),
|
||||
|
@ -87,9 +82,5 @@ fun LoadingLobstersItem() {
|
|||
@Preview
|
||||
@Composable
|
||||
fun ShimmerListPreview() {
|
||||
LazyColumn {
|
||||
items(10) {
|
||||
LoadingLobstersItem()
|
||||
}
|
||||
}
|
||||
LazyColumn { items(10) { LoadingLobstersItem() } }
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ import androidx.compose.ui.graphics.Color
|
|||
|
||||
val titleColor = Color(0xFF7395D9)
|
||||
|
||||
val lightColors = lightColors(
|
||||
val lightColors =
|
||||
lightColors(
|
||||
primary = Color.White,
|
||||
secondary = Color(0xFF6C0000),
|
||||
background = Color.White,
|
||||
|
@ -20,7 +21,8 @@ val lightColors = lightColors(
|
|||
onSurface = Color.White,
|
||||
)
|
||||
|
||||
val darkColors = darkColors(
|
||||
val darkColors =
|
||||
darkColors(
|
||||
primary = Color.White,
|
||||
secondary = Color(0xFFD2362D),
|
||||
background = Color.Black,
|
||||
|
|
|
@ -20,27 +20,38 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class LobstersViewModel @Inject constructor(
|
||||
class LobstersViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val lobstersRepository: LobstersRepository,
|
||||
private val clawPreferences: ClawPreferences,
|
||||
) : ViewModel() {
|
||||
private val _savedPosts = MutableStateFlow<List<SavedPost>>(emptyList())
|
||||
val savedPosts = _savedPosts.asStateFlow()
|
||||
val hottestPosts = Pager(PagingConfig(25)) {
|
||||
val hottestPosts =
|
||||
Pager(PagingConfig(25)) {
|
||||
HottestPostsPagingSource(lobstersRepository).also { hottestPostsPagingSource = it }
|
||||
}.flow.cachedIn(viewModelScope)
|
||||
val newestPosts = Pager(PagingConfig(25)) {
|
||||
}
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
val newestPosts =
|
||||
Pager(PagingConfig(25)) {
|
||||
NewestPostsPagingSource(lobstersRepository).also { newestPostsPagingSource = it }
|
||||
}.flow.cachedIn(viewModelScope)
|
||||
}
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
private var hottestPostsPagingSource: HottestPostsPagingSource? = null
|
||||
private var newestPostsPagingSource: NewestPostsPagingSource? = null
|
||||
|
||||
init {
|
||||
lobstersRepository.isCacheReady.onEach { ready ->
|
||||
lobstersRepository
|
||||
.isCacheReady
|
||||
.onEach { ready ->
|
||||
if (ready) {
|
||||
_savedPosts.value = lobstersRepository.getAllPostsFromCache()
|
||||
}
|
||||
}.launchIn(viewModelScope)
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
fun getSortOrder(): Flow<Boolean> {
|
||||
|
|
|
@ -3,9 +3,7 @@ package dev.msfjarvis.lobsters.util
|
|||
import dev.msfjarvis.lobsters.data.local.SavedPost
|
||||
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 {
|
||||
return SavedPost(
|
||||
shortId = shortId,
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
plugins {
|
||||
`lobsters-plugin`
|
||||
}
|
||||
plugins { `lobsters-plugin` }
|
||||
|
||||
subprojects {
|
||||
configurations.configureEach {
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
plugins { `kotlin-dsl` }
|
||||
|
||||
repositories {
|
||||
google()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
kotlinDslPluginOptions {
|
||||
experimentalWarning.set(false)
|
||||
}
|
||||
kotlinDslPluginOptions { experimentalWarning.set(false) }
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
plugins { `kotlin-dsl` }
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
|
@ -8,9 +6,7 @@ repositories {
|
|||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
kotlinDslPluginOptions {
|
||||
experimentalWarning.set(false)
|
||||
}
|
||||
kotlinDslPluginOptions { experimentalWarning.set(false) }
|
||||
|
||||
// force compilation of Dependencies.kt so it can be referenced in buildSrc/build.gradle.kts
|
||||
sourceSets.main {
|
||||
|
|
|
@ -18,14 +18,12 @@ import org.gradle.kotlin.dsl.withType
|
|||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
/**
|
||||
* Configure root project.
|
||||
* Note that classpath dependencies still need to be defined in the `buildscript` block in the top-level build.gradle.kts file.
|
||||
* Configure root project. Note that classpath dependencies still need to be defined in the
|
||||
* `buildscript` block in the top-level build.gradle.kts file.
|
||||
*/
|
||||
internal fun Project.configureForRootProject() {
|
||||
// register task for cleaning the build directory in the root project
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.buildDir)
|
||||
}
|
||||
tasks.register<Delete>("clean") { delete(rootProject.buildDir) }
|
||||
tasks.withType<Wrapper> {
|
||||
gradleVersion = "7.0-rc-1"
|
||||
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() {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
jcenter {
|
||||
content {
|
||||
includeGroup("org.jetbrains.compose.*")
|
||||
}
|
||||
}
|
||||
jcenter { content { includeGroup("org.jetbrains.compose.*") } }
|
||||
maven("https://dl.bintray.com/kotlin/kotlinx") {
|
||||
name = "KotlinX Bintray"
|
||||
content {
|
||||
|
@ -62,22 +54,20 @@ internal fun Project.configureForAllProjects() {
|
|||
}
|
||||
tasks.withType<Test> {
|
||||
maxParallelForks = Runtime.getRuntime().availableProcessors() * 2
|
||||
testLogging {
|
||||
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
|
||||
}
|
||||
testLogging { events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply configuration options for Android Application projects.
|
||||
*/
|
||||
/** Apply configuration options for Android Application projects. */
|
||||
@Suppress("UnstableApiUsage")
|
||||
internal fun BaseAppModuleExtension.configureAndroidApplicationOptions(project: Project) {
|
||||
val minifySwitch =
|
||||
project.providers.environmentVariable("DISABLE_MINIFY").forUseAtConfigurationTime()
|
||||
project.tasks.withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||
freeCompilerArgs =
|
||||
freeCompilerArgs +
|
||||
listOf(
|
||||
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-Xopt-in=androidx.compose.material.ExperimentalMaterialApi"
|
||||
)
|
||||
|
@ -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")
|
||||
internal fun TestedExtension.configureCommonAndroidOptions() {
|
||||
compileSdkVersion(30)
|
||||
|
|
|
@ -9,7 +9,8 @@ import org.gradle.kotlin.dsl.withType
|
|||
/**
|
||||
* 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 {
|
||||
* `core-library-desugaring`
|
||||
|
|
|
@ -2,9 +2,7 @@ import org.gradle.api.Project
|
|||
import org.gradle.kotlin.dsl.configure
|
||||
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() {
|
||||
extensions.configure<KaptExtension> {
|
||||
javacOptions {
|
||||
|
@ -17,15 +15,14 @@ internal fun Project.configureKapt() {
|
|||
}
|
||||
}
|
||||
// disable kapt tasks for unit tests
|
||||
tasks.matching {
|
||||
it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin")
|
||||
}.configureEach { enabled = false }
|
||||
tasks
|
||||
.matching { it.name.startsWith("kapt") && it.name.endsWith("UnitTestKotlin") }
|
||||
.configureEach { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
private val Project.hasDaggerCompilerDependency: Boolean
|
||||
get() = configurations.any {
|
||||
it.dependencies.any { dependency ->
|
||||
dependency.name == "dagger-compiler"
|
||||
}
|
||||
get() =
|
||||
configurations.any {
|
||||
it.dependencies.any { dependency -> dependency.name == "dagger-compiler" }
|
||||
}
|
||||
|
|
|
@ -3,7 +3,5 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
internal val additionalCompilerArgs = listOf(
|
||||
"-Xopt-in=kotlin.RequiresOptIn",
|
||||
"-Xskip-prerelease-check"
|
||||
)
|
||||
internal val additionalCompilerArgs =
|
||||
listOf("-Xopt-in=kotlin.RequiresOptIn", "-Xskip-prerelease-check")
|
||||
|
|
|
@ -29,8 +29,7 @@ class LobstersPlugin : Plugin<Project> {
|
|||
|
||||
project.plugins.all {
|
||||
when (this) {
|
||||
is JavaPlugin,
|
||||
is JavaLibraryPlugin -> {
|
||||
is JavaPlugin, is JavaLibraryPlugin -> {
|
||||
project.tasks.withType<JavaCompile> {
|
||||
options.compilerArgs.add("-Xlint:unchecked")
|
||||
options.isDeprecation = true
|
||||
|
@ -42,7 +41,9 @@ class LobstersPlugin : Plugin<Project> {
|
|||
is AppPlugin -> {
|
||||
project.extensions.getByType<TestedExtension>().configureCommonAndroidOptions()
|
||||
project.extensions.getByType<BaseAppModuleExtension>().configureBuildSigning(project)
|
||||
project.extensions.getByType<BaseAppModuleExtension>()
|
||||
project
|
||||
.extensions
|
||||
.getByType<BaseAppModuleExtension>()
|
||||
.configureAndroidApplicationOptions(project)
|
||||
}
|
||||
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
|
||||
|
|
|
@ -9,9 +9,7 @@ import org.gradle.api.Project
|
|||
|
||||
private const val KEYSTORE_CONFIG_PATH = "keystore.properties"
|
||||
|
||||
/**
|
||||
* Configure signing for all build types.
|
||||
*/
|
||||
/** Configure signing for all build types. */
|
||||
@Suppress("UnstableApiUsage")
|
||||
internal fun BaseAppModuleExtension.configureBuildSigning(project: Project) {
|
||||
with(project) {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
|
||||
import com.android.build.gradle.internal.plugins.AppPlugin
|
||||
import com.github.zafarkhaja.semver.Version
|
||||
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_VERSION_NAME = "versioning-plugin.versionName"
|
||||
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.
|
||||
"""
|
||||
|
||||
/**
|
||||
* 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
|
||||
* the [Project.getBuildDir] directory. It also adds Gradle tasks to bump the major, minor, and patch
|
||||
* versions along with one to prepare the next snapshot.
|
||||
* the [Project.getBuildDir] directory. It also adds Gradle tasks to bump the major, minor, and
|
||||
* patch versions along with one to prepare the next snapshot.
|
||||
*/
|
||||
@Suppress(
|
||||
"UnstableApiUsage",
|
||||
"NAME_SHADOWING"
|
||||
)
|
||||
@Suppress("UnstableApiUsage", "NAME_SHADOWING")
|
||||
class VersioningPlugin : Plugin<Project> {
|
||||
|
||||
/**
|
||||
* Generate the Android 'versionCode' property
|
||||
*/
|
||||
/** Generate the Android 'versionCode' property */
|
||||
private fun Version.androidCode(): Int {
|
||||
return majorVersion * 1_00_00 +
|
||||
minorVersion * 1_00 +
|
||||
patchVersion
|
||||
return majorVersion * 1_00_00 + 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) {
|
||||
val newVersionCode = androidCode()
|
||||
val props = Properties()
|
||||
|
@ -50,16 +41,15 @@ class VersioningPlugin : Plugin<Project> {
|
|||
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 {
|
||||
return Version.forIntegers(majorVersion, minorVersion, patchVersion)
|
||||
}
|
||||
|
||||
override fun apply(project: Project) {
|
||||
with(project) {
|
||||
val appPlugin = requireNotNull(plugins.findPlugin(AppPlugin::class.java)) {
|
||||
val appPlugin =
|
||||
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)
|
||||
|
@ -68,7 +58,8 @@ class VersioningPlugin : Plugin<Project> {
|
|||
}
|
||||
val contents = providers.fileContents(propFile).asText.forUseAtConfigurationTime()
|
||||
val versionProps = Properties().also { it.load(contents.get().byteInputStream()) }
|
||||
val versionName = requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
|
||||
val versionName =
|
||||
requireNotNull(versionProps.getProperty(VERSIONING_PROP_VERSION_NAME)) {
|
||||
"version.properties must contain a '$VERSIONING_PROP_VERSION_NAME' property"
|
||||
}
|
||||
val versionCode =
|
||||
|
@ -80,32 +71,21 @@ class VersioningPlugin : Plugin<Project> {
|
|||
afterEvaluate {
|
||||
val version = Version.valueOf(versionName)
|
||||
tasks.register("clearPreRelease") {
|
||||
doLast {
|
||||
version.clearPreRelease()
|
||||
.writeForAndroid(propFile.asFile.outputStream())
|
||||
}
|
||||
doLast { version.clearPreRelease().writeForAndroid(propFile.asFile.outputStream()) }
|
||||
}
|
||||
tasks.register("bumpMajor") {
|
||||
doLast {
|
||||
version.incrementMajorVersion()
|
||||
.writeForAndroid(propFile.asFile.outputStream())
|
||||
}
|
||||
doLast { version.incrementMajorVersion().writeForAndroid(propFile.asFile.outputStream()) }
|
||||
}
|
||||
tasks.register("bumpMinor") {
|
||||
doLast {
|
||||
version.incrementMinorVersion()
|
||||
.writeForAndroid(propFile.asFile.outputStream())
|
||||
}
|
||||
doLast { version.incrementMinorVersion().writeForAndroid(propFile.asFile.outputStream()) }
|
||||
}
|
||||
tasks.register("bumpPatch") {
|
||||
doLast {
|
||||
version.incrementPatchVersion()
|
||||
.writeForAndroid(propFile.asFile.outputStream())
|
||||
}
|
||||
doLast { version.incrementPatchVersion().writeForAndroid(propFile.asFile.outputStream()) }
|
||||
}
|
||||
tasks.register("bumpSnapshot") {
|
||||
doLast {
|
||||
version.incrementMinorVersion()
|
||||
version
|
||||
.incrementMinorVersion()
|
||||
.setPreReleaseVersion("SNAPSHOT")
|
||||
.writeForAndroid(propFile.asFile.outputStream())
|
||||
}
|
||||
|
|
|
@ -5,9 +5,7 @@ plugins {
|
|||
`lobsters-plugin`
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
}
|
||||
repositories { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
||||
|
||||
// workaround for https://youtrack.jetbrains.com/issue/KT-43944
|
||||
android {
|
||||
|
@ -40,23 +38,11 @@ kotlin {
|
|||
}
|
||||
}
|
||||
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
implementation(compose.runtime)
|
||||
}
|
||||
}
|
||||
val jvmMain by getting { dependencies { implementation(compose.runtime) } }
|
||||
|
||||
val androidTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-junit"))
|
||||
}
|
||||
}
|
||||
val androidTest by getting { dependencies { implementation(kotlin("test-junit")) } }
|
||||
|
||||
val jvmTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test-junit"))
|
||||
}
|
||||
}
|
||||
val jvmTest by getting { dependencies { implementation(kotlin("test-junit")) } }
|
||||
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
|
@ -68,9 +54,7 @@ kotlin {
|
|||
}
|
||||
|
||||
android {
|
||||
buildFeatures {
|
||||
androidResources = true
|
||||
}
|
||||
buildFeatures { androidResources = true }
|
||||
|
||||
sourceSets {
|
||||
named("main") {
|
||||
|
|
|
@ -6,7 +6,8 @@ import androidx.browser.customtabs.CustomTabsIntent
|
|||
|
||||
actual class UrlLauncher(private val context: Context) {
|
||||
actual fun launch(url: String) {
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
val customTabsIntent =
|
||||
CustomTabsIntent.Builder()
|
||||
.setShareState(CustomTabsIntent.SHARE_STATE_ON)
|
||||
.setShowTitle(true)
|
||||
.setColorScheme(CustomTabsIntent.COLOR_SCHEME_DARK)
|
||||
|
|
|
@ -2,8 +2,6 @@ package dev.msfjarvis.lobsters.utils
|
|||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
expect fun Strings.get(): String
|
||||
@Composable expect fun Strings.get(): String
|
||||
|
||||
@Composable
|
||||
expect fun Strings.get(fmt: Any): String
|
||||
@Composable expect fun Strings.get(fmt: Any): String
|
||||
|
|
|
@ -13,5 +13,4 @@ enum class Strings {
|
|||
SavedPosts,
|
||||
SubmittedBy,
|
||||
NewestPosts,
|
||||
;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,8 @@ class SqlDelightQueriesTest {
|
|||
fun setUp() {
|
||||
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
|
||||
LobstersDatabase.Schema.create(driver)
|
||||
val database = LobstersDatabase(
|
||||
val database =
|
||||
LobstersDatabase(
|
||||
driver,
|
||||
SavedPost.Adapter(TagsAdapter()),
|
||||
)
|
||||
|
@ -128,12 +129,12 @@ class SqlDelightQueriesTest {
|
|||
assertEquals(0, postsCount)
|
||||
}
|
||||
|
||||
|
||||
private fun createTestData(count: Int): ArrayList<SavedPost> {
|
||||
val posts = arrayListOf<SavedPost>()
|
||||
|
||||
for (i in 1..count) {
|
||||
val post = SavedPost(
|
||||
val post =
|
||||
SavedPost(
|
||||
shortId = "test_id_$i",
|
||||
createdAt = "0",
|
||||
title = "test",
|
||||
|
|
|
@ -6,9 +6,7 @@ plugins {
|
|||
`lobsters-plugin`
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||
}
|
||||
repositories { maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") }
|
||||
|
||||
dependencies {
|
||||
implementation(project(":api"))
|
||||
|
@ -22,8 +20,4 @@ dependencies {
|
|||
implementation(Dependencies.ThirdParty.Retrofit.moshi)
|
||||
}
|
||||
|
||||
compose.desktop {
|
||||
application {
|
||||
mainClass = "dev.msfjarvis.lobsters.ui.Main"
|
||||
}
|
||||
}
|
||||
compose.desktop { application { mainClass = "dev.msfjarvis.lobsters.ui.Main" } }
|
||||
|
|
|
@ -8,9 +8,9 @@ import retrofit2.converter.moshi.MoshiConverterFactory
|
|||
import retrofit2.create
|
||||
|
||||
class ApiRepository {
|
||||
private val moshi = Moshi.Builder()
|
||||
.build()
|
||||
private val retrofit = Retrofit.Builder()
|
||||
private val moshi = Moshi.Builder().build()
|
||||
private val retrofit =
|
||||
Retrofit.Builder()
|
||||
.baseUrl(LobstersApi.BASE_URL)
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
|
|
|
@ -29,16 +29,11 @@ fun LobstersItem(
|
|||
post: SavedPost,
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
UrlLauncher.launch(post.url)
|
||||
}
|
||||
.wrapContentHeight(),
|
||||
modifier =
|
||||
Modifier.fillMaxWidth().clickable { UrlLauncher.launch(post.url) }.wrapContentHeight(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(start = 12.dp, end = 24.dp),
|
||||
modifier = Modifier.padding(start = 12.dp, end = 24.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
|
@ -49,26 +44,21 @@ fun LobstersItem(
|
|||
text = post.title,
|
||||
color = titleColor,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.padding(top = 4.dp),
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
TagRow(
|
||||
tags = post.tags,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp, bottom = 8.dp, end = 16.dp),
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, end = 16.dp),
|
||||
)
|
||||
Row {
|
||||
KamelImage(
|
||||
resource = lazyImageResource(data = URI(post.submitterAvatarUrl)),
|
||||
contentDescription = "${post.submitterName}'s avatar",
|
||||
modifier = Modifier
|
||||
.requiredWidth(30.dp)
|
||||
.padding(4.dp),
|
||||
modifier = Modifier.requiredWidth(30.dp).padding(4.dp),
|
||||
)
|
||||
Text(
|
||||
text = "Submitted by ${post.submitterName}",
|
||||
modifier = Modifier
|
||||
.padding(4.dp),
|
||||
modifier = Modifier.padding(4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -87,8 +77,8 @@ fun TagRow(
|
|||
tags.forEach { tag ->
|
||||
Text(
|
||||
text = tag,
|
||||
modifier = Modifier
|
||||
.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp))
|
||||
modifier =
|
||||
Modifier.background(Color(0xFFFFFCD7), RoundedCornerShape(8.dp))
|
||||
.padding(horizontal = 8.dp),
|
||||
color = Color.DarkGray,
|
||||
)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@file:JvmName("Main")
|
||||
|
||||
package dev.msfjarvis.lobsters.ui
|
||||
|
||||
import androidx.compose.desktop.Window
|
||||
|
@ -12,15 +13,15 @@ import androidx.compose.foundation.rememberScrollbarAdapter
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.Alignment
|
||||
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.local.SavedPost
|
||||
import dev.msfjarvis.lobsters.model.LobstersPost
|
||||
import dev.msfjarvis.lobsters.ui.urllauncher.LocalUrlLauncher
|
||||
import dev.msfjarvis.lobsters.ui.urllauncher.UrlLauncher
|
||||
|
@ -31,14 +32,13 @@ import kotlinx.coroutines.withContext
|
|||
val repository = ApiRepository()
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
fun main() = Window(title = "Claw for lobste.rs") {
|
||||
fun main() =
|
||||
Window(title = "Claw for lobste.rs") {
|
||||
val urlLauncher = UrlLauncher()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var items by remember { mutableStateOf(emptyList<SavedPost>()) }
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
items = repository.loadPosts(0).map(::toDbModel)
|
||||
}
|
||||
withContext(Dispatchers.IO) { items = repository.loadPosts(0).map(::toDbModel) }
|
||||
}
|
||||
LobstersTheme {
|
||||
Box(
|
||||
|
@ -46,19 +46,13 @@ fun main() = Window(title = "Claw for lobste.rs") {
|
|||
) {
|
||||
val stateVertical = rememberScrollState(0)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(stateVertical),
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(stateVertical),
|
||||
) {
|
||||
if (items.isEmpty()) {
|
||||
Text("Loading...")
|
||||
} else {
|
||||
CompositionLocalProvider(LocalUrlLauncher provides urlLauncher) {
|
||||
Column {
|
||||
items.forEach {
|
||||
LobstersItem(it)
|
||||
}
|
||||
}
|
||||
Column { items.forEach { LobstersItem(it) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@file:Suppress("UNUSED")
|
||||
|
||||
package dev.msfjarvis.lobsters.ui
|
||||
|
||||
import androidx.compose.material.MaterialTheme
|
||||
|
@ -9,7 +10,8 @@ import androidx.compose.ui.graphics.Color
|
|||
|
||||
val titleColor = Color(0xFF7395D9)
|
||||
|
||||
val lightColors = lightColors(
|
||||
val lightColors =
|
||||
lightColors(
|
||||
primary = Color.White,
|
||||
secondary = Color(0xFF6C0000),
|
||||
background = Color.White,
|
||||
|
@ -20,7 +22,8 @@ val lightColors = lightColors(
|
|||
onSurface = Color.White,
|
||||
)
|
||||
|
||||
val darkColors = darkColors(
|
||||
val darkColors =
|
||||
darkColors(
|
||||
primary = Color.White,
|
||||
secondary = Color(0xFF6C0000),
|
||||
background = Color.Black,
|
||||
|
@ -32,10 +35,7 @@ val darkColors = darkColors(
|
|||
)
|
||||
|
||||
@Composable
|
||||
fun LobstersTheme(
|
||||
useLightColors: Boolean = true,
|
||||
children: @Composable () -> Unit
|
||||
) {
|
||||
fun LobstersTheme(useLightColors: Boolean = true, children: @Composable () -> Unit) {
|
||||
MaterialTheme(
|
||||
colors = if (useLightColors) lightColors else darkColors,
|
||||
content = children,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
rootProject.name = "Claw"
|
||||
|
||||
include(":app", ":api", ":common", ":database", ":desktop")
|
||||
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue