Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ val localProps = Properties().apply {
val file = rootProject.file("local.properties")
if (file.exists()) file.inputStream().use { this.load(it) }
}
val localGithubClientId = (localProps.getProperty("GITHUB_CLIENT_ID") ?: "").trim()
val localGithubClientId = (localProps.getProperty("GITHUB_CLIENT_ID") ?: "Ov23linTY28VFpFjFiI9").trim()

// Generate BuildConfig for JVM (Configuration Cache Compatible)
val generateJvmBuildConfig = tasks.register("generateJvmBuildConfig") {
Expand Down Expand Up @@ -103,6 +103,8 @@ kotlin {

implementation(libs.androidx.datastore)
implementation(libs.androidx.datastore.preferences)

implementation(libs.liquid)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
Expand Down
Binary file modified composeApp/release/baselineProfiles/0/composeApp-release.dm
Binary file not shown.
Binary file modified composeApp/release/baselineProfiles/1/composeApp-release.dm
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class AuthRepositoryImpl(
var remainingMs = timeoutMs
var intervalMs = (start.intervalSec.coerceAtLeast(5)) * 1000L
var consecutiveErrors = 0
val maxConsecutiveErrors = 3
val maxConsecutiveErrors = 5

Logger.d { "⏱️ Starting token polling. Expires in: ${start.expiresInSec}s, Interval: ${start.intervalSec}s" }

Expand All @@ -61,14 +61,16 @@ class AuthRepositoryImpl(

if (success != null) {
Logger.d { "✅ Token received successfully!" }
tokenDataSource.save(success)
withRetry(maxAttempts = 3) {
tokenDataSource.save(success)
}
return@withContext success
}

val error = res.exceptionOrNull()
val msg = (error?.message ?: "").lowercase()

Logger.d { "📡 Poll response: $msg" }
Logger.d { "📡 Poll response: $msg (errors: $consecutiveErrors/$maxConsecutiveErrors)" }

when {
"authorization_pending" in msg -> {
Expand All @@ -95,44 +97,58 @@ class AuthRepositoryImpl(
)
}

"unable to resolve" in msg || "no address" in msg -> {
"unable to resolve" in msg ||
"no address" in msg ||
"failed to connect" in msg ||
"connection refused" in msg ||
"network is unreachable" in msg -> {
consecutiveErrors++
Logger.d { "⚠️ Network error, retrying... ($consecutiveErrors/$maxConsecutiveErrors)" }

val backoffDelay = intervalMs * (1 + consecutiveErrors)

if (consecutiveErrors >= maxConsecutiveErrors) {
throw Exception(
"Network connection lost during authentication. " +
"Network connection unstable during authentication. " +
"Please check your connection and try again."
)
}
Logger.d { "⚠️ Network error, retrying... ($consecutiveErrors/$maxConsecutiveErrors)" }
delay(intervalMs)
remainingMs -= intervalMs
delay(backoffDelay)
remainingMs -= backoffDelay
}

else -> {
consecutiveErrors++
Logger.d { "⚠️ Error: $msg (attempt $consecutiveErrors/$maxConsecutiveErrors)" }

if (consecutiveErrors >= maxConsecutiveErrors) {
throw Exception("Authentication failed: $msg")
}
Logger.d { "⚠️ Unknown error, retrying... ($consecutiveErrors/$maxConsecutiveErrors)" }
delay(intervalMs)
remainingMs -= intervalMs

val backoffDelay = intervalMs * 2
delay(backoffDelay)
remainingMs -= backoffDelay
}
}

} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Logger.d { "❌ Poll error: ${e.message}" }
Logger.d { "❌ Error type: ${e::class.simpleName}" }
consecutiveErrors++

if (consecutiveErrors >= maxConsecutiveErrors) {
throw Exception(
"Authentication failed after multiple attempts. " +
"Please try again.",
"Error: ${e.message}",
e
)
}
delay(intervalMs)
remainingMs -= intervalMs

val backoffDelay = intervalMs * (1 + consecutiveErrors)
delay(backoffDelay)
remainingMs -= backoffDelay
}
}

Expand All @@ -141,6 +157,22 @@ class AuthRepositoryImpl(
)
}

private suspend fun <T> withRetry(
maxAttempts: Int = 3,
initialDelay: Long = 1000,
block: suspend () -> T
): T {
repeat(maxAttempts - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
Logger.d { "⚠️ Retry attempt ${attempt + 1} failed: ${e.message}" }
delay(initialDelay * (attempt + 1))
}
}
return block()
}

override suspend fun logout() {
tokenDataSource.clear()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.OpenInBrowser
Expand All @@ -19,11 +20,16 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.github.fletchmckee.liquid.liquefiable
import io.github.fletchmckee.liquid.liquid
import io.github.fletchmckee.liquid.rememberLiquidState
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
import zed.rainxch.githubstore.core.presentation.theme.GithubStoreTheme
Expand All @@ -35,6 +41,7 @@ import zed.rainxch.githubstore.feature.details.presentation.components.sections.
import zed.rainxch.githubstore.feature.details.presentation.components.sections.stats
import zed.rainxch.githubstore.feature.details.presentation.components.sections.whatsNew
import zed.rainxch.githubstore.feature.details.presentation.components.states.ErrorState
import zed.rainxch.githubstore.feature.details.presentation.utils.LocalTopbarLiquidState

@Composable
fun DetailsRoot(
Expand Down Expand Up @@ -78,93 +85,106 @@ fun DetailsScreen(
state: DetailsState,
onAction: (DetailsAction) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = { },
navigationIcon = {
IconButton(
onClick = {
onAction(DetailsAction.OnNavigateBackClick)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Navigate Back",
modifier = Modifier.size(24.dp)
)
}
},
actions = {
state.repository?.htmlUrl?.let {
val liquidTopbarState = rememberLiquidState()

CompositionLocalProvider(
value = LocalTopbarLiquidState provides liquidTopbarState
) {
Scaffold(
topBar = {
TopAppBar(
title = { },
navigationIcon = {
IconButton(
onClick = {
onAction(DetailsAction.OpenRepoInBrowser)
},
onAction(DetailsAction.OnNavigateBackClick)
}
) {
Icon(
imageVector = Icons.Default.OpenInBrowser,
contentDescription = "Open repository",
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Navigate Back",
modifier = Modifier.size(24.dp)
)
}
},
actions = {
state.repository?.htmlUrl?.let {
IconButton(
onClick = {
onAction(DetailsAction.OpenRepoInBrowser)
},
) {
Icon(
imageVector = Icons.Default.OpenInBrowser,
contentDescription = "Open repository",
modifier = Modifier.size(24.dp)
)
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent
),
modifier = Modifier.liquid(liquidTopbarState) {
this.shape = CutCornerShape(0.dp)
this.frost = 20.dp
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.background
)
)
},
containerColor = MaterialTheme.colorScheme.background
) { innerPadding ->

if (state.isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
},
containerColor = MaterialTheme.colorScheme.background
) { innerPadding ->

if (state.isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}

return@Scaffold
}

return@Scaffold
}
if (state.errorMessage != null) {
ErrorState(state.errorMessage, onAction)

if (state.errorMessage != null) {
ErrorState(state.errorMessage, onAction)
return@Scaffold
}

return@Scaffold
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
header(
state = state,
onAction = onAction,
)

LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
header(state, onAction)

state.stats?.let { stats ->
stats(repoStats = stats)
}
state.stats?.let { stats ->
stats(repoStats = stats)
}

state.readmeMarkdown?.let { readmeMarkdown ->
about(readmeMarkdown)
}
state.readmeMarkdown?.let { readmeMarkdown ->
about(readmeMarkdown)
}

state.latestRelease?.let { latestRelease ->
whatsNew(latestRelease)
}
state.latestRelease?.let { latestRelease ->
whatsNew(latestRelease)
}

state.userProfile?.let { userProfile ->
author(
author = userProfile,
onAction = onAction
)
}
state.userProfile?.let { userProfile ->
author(
author = userProfile,
onAction = onAction
)
}

if (state.installLogs.isNotEmpty()) {
logs(state)
if (state.installLogs.isNotEmpty()) {
logs(state)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import io.github.fletchmckee.liquid.liquefiable
import org.jetbrains.compose.ui.tooling.preview.Preview
import zed.rainxch.githubstore.core.domain.model.Architecture
import zed.rainxch.githubstore.core.domain.model.GithubAsset
import zed.rainxch.githubstore.feature.details.presentation.DetailsAction
import zed.rainxch.githubstore.feature.details.presentation.DetailsState
import zed.rainxch.githubstore.feature.details.presentation.DownloadStage
import zed.rainxch.githubstore.feature.details.presentation.utils.LocalTopbarLiquidState
import zed.rainxch.githubstore.feature.details.presentation.utils.extractArchitectureFromName
import zed.rainxch.githubstore.feature.details.presentation.utils.isExactArchitectureMatch

Expand All @@ -57,6 +59,8 @@ fun SmartInstallButton(
modifier: Modifier = Modifier,
state: DetailsState
) {
val liquidState = LocalTopbarLiquidState.current

val enabled = remember(primaryAsset, isDownloading, isInstalling) {
primaryAsset != null && !isDownloading && !isInstalling
}
Expand Down Expand Up @@ -86,7 +90,8 @@ fun SmartInstallButton(
onClick = {
onAction(DetailsAction.InstallPrimary)
}
),
)
.liquefiable(liquidState),
colors = CardDefaults.elevatedCardColors(
containerColor = if (enabled) {
MaterialTheme.colorScheme.primary
Expand Down
Loading