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.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"
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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(),
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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 = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>() {
|
||||||
|
|
||||||
|
|
|
@ -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>() {
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -13,7 +13,6 @@ object MoshiModule {
|
||||||
@Provides
|
@Provides
|
||||||
@Reusable
|
@Reusable
|
||||||
fun provideMoshi(): Moshi {
|
fun provideMoshi(): Moshi {
|
||||||
return Moshi.Builder()
|
return Moshi.Builder().build()
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,8 +62,7 @@ fun LobstersApp() {
|
||||||
newestPostsListState.animateScrollToItem(index)
|
newestPostsListState.animateScrollToItem(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() }
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
plugins {
|
plugins { `lobsters-plugin` }
|
||||||
`lobsters-plugin`
|
|
||||||
}
|
|
||||||
|
|
||||||
subprojects {
|
subprojects {
|
||||||
configurations.configureEach {
|
configurations.configureEach {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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" }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -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"
|
|
||||||
)
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -13,5 +13,4 @@ enum class Strings {
|
||||||
SavedPosts,
|
SavedPosts,
|
||||||
SubmittedBy,
|
SubmittedBy,
|
||||||
NewestPosts,
|
NewestPosts,
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue