Feat: Commit list in home tab

This commit is contained in:
wingio 2023-03-20 14:27:20 -04:00
parent c9bf49d395
commit 3bfeb662f0
10 changed files with 245 additions and 6 deletions

View file

@ -80,10 +80,11 @@ android {
dependencies { dependencies {
implementation("androidx.core:core-ktx:1.9.0") implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1") implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.0")
implementation("androidx.paging:paging-compose:1.0.0-alpha18")
implementation(platform("androidx.compose:compose-bom:2022.10.00")) implementation(platform("androidx.compose:compose-bom:2022.10.00"))
implementation("androidx.activity:activity-compose:1.5.1") implementation("androidx.activity:activity-compose:1.6.1")
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
@ -108,6 +109,11 @@ dependencies {
implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion") implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion")
implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion") implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion")
val coilVersion = "2.2.2"
implementation("io.coil-kt:coil:$coilVersion")
implementation("io.coil-kt:coil-compose:$coilVersion")
val ktorVersion = "2.1.1" val ktorVersion = "2.1.1"
implementation("io.ktor:ktor-client-core:$ktorVersion") implementation("io.ktor:ktor-client-core:$ktorVersion")
@ -117,7 +123,6 @@ dependencies {
implementation("io.ktor:ktor-client-logging:$ktorVersion") implementation("io.ktor:ktor-client-logging:$ktorVersion")
implementation("io.github.diamondminer88:zip-android:2.1.0@aar") implementation("io.github.diamondminer88:zip-android:2.1.0@aar")
// implementation("com.android.tools.build:apksig:7.4.0-beta04")
implementation(files("libs/lspatch.jar")) implementation(files("libs/lspatch.jar"))
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")

View file

@ -18,4 +18,6 @@ class RestRepository(
) )
} }
suspend fun getCommits(repo: String, page: Int = 1) = service.getCommits(repo, page)
} }

View file

@ -0,0 +1,19 @@
package dev.beefers.vendetta.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Commit(
val sha: String,
@SerialName("commit") val info: Info,
@SerialName("html_url") val url: String,
val author: User
) {
@Serializable
data class Info(
val message: String
)
}

View file

@ -0,0 +1,10 @@
package dev.beefers.vendetta.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class User(
@SerialName("login") val username: String,
@SerialName("avatar_url") val avatar: String
)

View file

@ -1,7 +1,9 @@
package dev.beefers.vendetta.manager.network.service package dev.beefers.vendetta.manager.network.service
import dev.beefers.vendetta.manager.network.dto.Commit
import dev.beefers.vendetta.manager.network.dto.Index import dev.beefers.vendetta.manager.network.dto.Index
import dev.beefers.vendetta.manager.network.dto.Release import dev.beefers.vendetta.manager.network.dto.Release
import io.ktor.client.request.parameter
import io.ktor.client.request.url import io.ktor.client.request.url
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -22,4 +24,11 @@ class RestService(
} }
} }
suspend fun getCommits(repo: String, page: Int = 1) = withContext(Dispatchers.IO) {
httpService.request<List<Commit>> {
url("https://api.github.com/repos/vendetta-mod/$repo/commits")
parameter("page", page)
}
}
} }

View file

@ -25,6 +25,17 @@ inline fun <D> ApiResponse<D>.ifSuccessful(block: (D) -> Unit) {
if (this is ApiResponse.Success) block(data) if (this is ApiResponse.Success) block(data)
} }
fun <D> ApiResponse<D>.fold(
onSuccess: (D) -> Unit = {},
onError: () -> Unit = {}
) {
when(this) {
is ApiResponse.Success -> onSuccess(data)
is ApiResponse.Error,
is ApiResponse.Failure -> onError()
}
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T, R> ApiResponse<T>.transform(block: (T) -> R): ApiResponse<R> { fun <T, R> ApiResponse<T>.transform(block: (T) -> R): ApiResponse<R> {
return when (this) { return when (this) {

View file

@ -4,11 +4,15 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -18,6 +22,9 @@ import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.OpenInNew import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Home
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -30,6 +37,10 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import androidx.paging.compose.itemsIndexed
import cafe.adriel.voyager.koin.getScreenModel import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.navigator.currentOrThrow
@ -39,6 +50,7 @@ import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
import dev.beefers.vendetta.manager.ui.components.SegmentedButton import dev.beefers.vendetta.manager.ui.components.SegmentedButton
import dev.beefers.vendetta.manager.ui.screen.installer.InstallerScreen import dev.beefers.vendetta.manager.ui.screen.installer.InstallerScreen
import dev.beefers.vendetta.manager.ui.viewmodel.home.HomeViewModel import dev.beefers.vendetta.manager.ui.viewmodel.home.HomeViewModel
import dev.beefers.vendetta.manager.ui.widgets.home.Commit
import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.DiscordVersion
import dev.beefers.vendetta.manager.utils.ManagerTab import dev.beefers.vendetta.manager.utils.ManagerTab
import dev.beefers.vendetta.manager.utils.TabOptions import dev.beefers.vendetta.manager.utils.TabOptions
@ -150,10 +162,72 @@ class HomeScreen : ManagerTab {
} }
} }
Column( ElevatedCard(
verticalArrangement = Arrangement.spacedBy(8.dp) modifier = Modifier
.fillMaxSize()
) { ) {
val commits = viewModel.commits.collectAsLazyPagingItems()
val loading = commits.loadState.append is LoadState.Loading || commits.loadState.refresh is LoadState.Loading
val failed = commits.loadState.append is LoadState.Error || commits.loadState.refresh is LoadState.Error
LazyColumn {
itemsIndexed(
items = commits,
key = { _, commit -> commit.sha }
) { i, commit ->
if (commit != null) {
Column {
Commit(commit = commit)
if(i < commits.itemSnapshotList.lastIndex) {
Divider(
thickness = 0.5.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
}
}
if(loading) {
item {
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
CircularProgressIndicator(
strokeWidth = 3.dp,
modifier = Modifier.size(30.dp)
)
}
}
}
if(failed) {
item {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = stringResource(R.string.msg_load_fail),
style = MaterialTheme.typography.labelLarge,
textAlign = TextAlign.Center
)
Button(onClick = { commits.retry() }) {
Text(stringResource(R.string.action_retry))
}
}
}
}
}
} }
} }
} }

View file

@ -7,12 +7,20 @@ import android.provider.Settings
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.cachedIn
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.InstallManager
import dev.beefers.vendetta.manager.domain.manager.PreferenceManager import dev.beefers.vendetta.manager.domain.manager.PreferenceManager
import dev.beefers.vendetta.manager.domain.repository.RestRepository import dev.beefers.vendetta.manager.domain.repository.RestRepository
import dev.beefers.vendetta.manager.network.dto.Commit
import dev.beefers.vendetta.manager.network.utils.ApiResponse
import dev.beefers.vendetta.manager.network.utils.dataOrNull import dev.beefers.vendetta.manager.network.utils.dataOrNull
import dev.beefers.vendetta.manager.network.utils.fold
import dev.beefers.vendetta.manager.utils.DiscordVersion import dev.beefers.vendetta.manager.utils.DiscordVersion
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -26,6 +34,29 @@ class HomeViewModel(
var discordVersions by mutableStateOf<Map<DiscordVersion.Type, DiscordVersion?>?>(null) var discordVersions by mutableStateOf<Map<DiscordVersion.Type, DiscordVersion?>?>(null)
private set private set
val commits = Pager(PagingConfig(pageSize = 30)) {
object : PagingSource<Int, Commit>() {
override fun getRefreshKey(state: PagingState<Int, Commit>): Int? =
state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Commit> {
val page = params.key ?: 0
return when(val response = repo.getCommits("Vendetta", page)) {
is ApiResponse.Success -> LoadResult.Page(
data = response.data,
prevKey = if (page > 0) page - 1 else null,
nextKey = if (response.data.isNotEmpty()) page + 1 else null
)
is ApiResponse.Failure -> LoadResult.Error(response.error)
is ApiResponse.Error -> LoadResult.Error(response.error)
}
}
}
}.flow.cachedIn(coroutineScope)
init { init {
getDiscordVersions() getDiscordVersions()
} }
@ -57,7 +88,6 @@ class HomeViewModel(
context.startActivity(this) context.startActivity(this)
} }
} }
} }
} }

View file

@ -0,0 +1,76 @@
package dev.beefers.vendetta.manager.ui.widgets.home
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import dev.beefers.vendetta.manager.network.dto.Commit
@Composable
fun Commit(
commit: Commit
) {
val uriHandler = LocalUriHandler.current
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.fillMaxWidth()
.clickable { uriHandler.openUri(commit.url) }
.padding(16.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = commit.author.avatar,
contentDescription = commit.author.username,
modifier = Modifier
.size(20.dp)
.clip(CircleShape)
)
Text(
text = commit.author.username,
style = MaterialTheme.typography.labelMedium
)
}
Text(
"",
style = MaterialTheme.typography.labelLarge
)
Text(
text = commit.sha.substring(0, 7),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
fontFamily = FontFamily.Monospace
)
}
Text(
text = commit.info.message.split("\n").first(),
style = MaterialTheme.typography.labelLarge
)
}
}

View file

@ -42,6 +42,7 @@
<string name="action_launch">Launch</string> <string name="action_launch">Launch</string>
<string name="action_uninstall">Uninstall</string> <string name="action_uninstall">Uninstall</string>
<string name="action_info">Info</string> <string name="action_info">Info</string>
<string name="action_retry">Retry</string>
<string name="installer_cached">Cached</string> <string name="installer_cached">Cached</string>
@ -70,4 +71,6 @@
<string name="channel_stable">Stable</string> <string name="channel_stable">Stable</string>
<string name="channel_beta">Beta</string> <string name="channel_beta">Beta</string>
<string name="channel_alpha">Alpha</string> <string name="channel_alpha">Alpha</string>
<string name="msg_load_fail">Failed to load commits</string>
</resources> </resources>