From 3bfeb662f0d02c8a41a421582faefc351bc4d17e Mon Sep 17 00:00:00 2001 From: wingio Date: Mon, 20 Mar 2023 14:27:20 -0400 Subject: [PATCH] Feat: Commit list in home tab --- app/build.gradle.kts | 11 ++- .../domain/repository/RestRepository.kt | 2 + .../vendetta/manager/network/dto/Commit.kt | 19 +++++ .../vendetta/manager/network/dto/User.kt | 10 +++ .../manager/network/service/RestService.kt | 9 +++ .../manager/network/utils/ApiResponse.kt | 11 +++ .../manager/ui/screen/home/HomeScreen.kt | 78 ++++++++++++++++++- .../ui/viewmodel/home/HomeViewModel.kt | 32 +++++++- .../manager/ui/widgets/home/Commit.kt | 76 ++++++++++++++++++ app/src/main/res/values/strings.xml | 3 + 10 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/dev/beefers/vendetta/manager/network/dto/Commit.kt create mode 100644 app/src/main/java/dev/beefers/vendetta/manager/network/dto/User.kt create mode 100644 app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/home/Commit.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index accc489..13b02ff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,10 +80,11 @@ android { dependencies { 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("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-graphics") implementation("androidx.compose.ui:ui-tooling-preview") @@ -108,6 +109,11 @@ dependencies { implementation("cafe.adriel.voyager:voyager-transitions:$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" implementation("io.ktor:ktor-client-core:$ktorVersion") @@ -117,7 +123,6 @@ dependencies { implementation("io.ktor:ktor-client-logging:$ktorVersion") 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")) testImplementation("junit:junit:4.13.2") diff --git a/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt b/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt index 2617665..be0b18b 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/domain/repository/RestRepository.kt @@ -18,4 +18,6 @@ class RestRepository( ) } + suspend fun getCommits(repo: String, page: Int = 1) = service.getCommits(repo, page) + } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/network/dto/Commit.kt b/app/src/main/java/dev/beefers/vendetta/manager/network/dto/Commit.kt new file mode 100644 index 0000000..76c0095 --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/network/dto/Commit.kt @@ -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 + ) + +} diff --git a/app/src/main/java/dev/beefers/vendetta/manager/network/dto/User.kt b/app/src/main/java/dev/beefers/vendetta/manager/network/dto/User.kt new file mode 100644 index 0000000..029e370 --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/network/dto/User.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt b/app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt index faf62f7..c6429c1 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/network/service/RestService.kt @@ -1,7 +1,9 @@ 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.Release +import io.ktor.client.request.parameter import io.ktor.client.request.url import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -22,4 +24,11 @@ class RestService( } } + suspend fun getCommits(repo: String, page: Int = 1) = withContext(Dispatchers.IO) { + httpService.request> { + url("https://api.github.com/repos/vendetta-mod/$repo/commits") + parameter("page", page) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt b/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt index 794e717..efdf6f7 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/network/utils/ApiResponse.kt @@ -25,6 +25,17 @@ inline fun ApiResponse.ifSuccessful(block: (D) -> Unit) { if (this is ApiResponse.Success) block(data) } +fun ApiResponse.fold( + onSuccess: (D) -> Unit = {}, + onError: () -> Unit = {} +) { + when(this) { + is ApiResponse.Success -> onSuccess(data) + is ApiResponse.Error, + is ApiResponse.Failure -> onError() + } +} + @Suppress("UNCHECKED_CAST") fun ApiResponse.transform(block: (T) -> R): ApiResponse { return when (this) { diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt index 3317c6e..c882df2 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/screen/home/HomeScreen.kt @@ -4,11 +4,15 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding 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.RoundedCornerShape 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.outlined.Home 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.MaterialTheme 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.text.style.TextAlign 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.navigator.LocalNavigator 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.screen.installer.InstallerScreen 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.ManagerTab import dev.beefers.vendetta.manager.utils.TabOptions @@ -150,10 +162,72 @@ class HomeScreen : ManagerTab { } } - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) + ElevatedCard( + 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)) + } + } + } + } + } } } } diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt index e31645d..6ea2d23 100644 --- a/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/viewmodel/home/HomeViewModel.kt @@ -7,12 +7,20 @@ import android.provider.Settings import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.coroutineScope import dev.beefers.vendetta.manager.domain.manager.InstallManager import dev.beefers.vendetta.manager.domain.manager.PreferenceManager 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.fold import dev.beefers.vendetta.manager.utils.DiscordVersion import kotlinx.coroutines.launch @@ -26,6 +34,29 @@ class HomeViewModel( var discordVersions by mutableStateOf?>(null) private set + val commits = Pager(PagingConfig(pageSize = 30)) { + object : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey + } + + override suspend fun load(params: LoadParams): LoadResult { + 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 { getDiscordVersions() } @@ -57,7 +88,6 @@ class HomeViewModel( context.startActivity(this) } } - } } \ No newline at end of file diff --git a/app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/home/Commit.kt b/app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/home/Commit.kt new file mode 100644 index 0000000..5ee1806 --- /dev/null +++ b/app/src/main/java/dev/beefers/vendetta/manager/ui/widgets/home/Commit.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14f3738..580c6c8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,7 @@ Launch Uninstall Info + Retry Cached @@ -70,4 +71,6 @@ Stable Beta Alpha + + Failed to load commits \ No newline at end of file