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
1 change: 0 additions & 1 deletion composeApp/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
<uses-permission
android:name="android.permission.REQUEST_INSTALL_PACKAGES"
tools:ignore="RequestInstallPackagesPolicy" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />

<application
android:name=".app.GithubStoreApp"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
Expand All @@ -21,6 +27,7 @@ import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf
import zed.rainxch.apps.presentation.AppsRoot
import zed.rainxch.auth.presentation.AuthenticationRoot
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
import zed.rainxch.details.presentation.DetailsRoot
import zed.rainxch.devprofile.presentation.DeveloperProfileRoot
Expand All @@ -35,9 +42,12 @@ fun AppNavigation(
navController: NavHostController
) {
val liquidState = rememberLiquidState()
var bottomNavigationHeight by remember { mutableStateOf(0.dp) }
val density = LocalDensity.current

CompositionLocalProvider(
value = LocalBottomNavigationLiquid provides liquidState
LocalBottomNavigationLiquid provides liquidState,
LocalBottomNavigationHeight provides bottomNavigationHeight
) {
Box(
modifier = Modifier.fillMaxSize()
Expand Down Expand Up @@ -235,6 +245,9 @@ fun AppNavigation(
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(bottom = 24.dp)
.onGloballyPositioned { coordinates ->
bottomNavigationHeight = with(density) { coordinates.size.height.toDp() }
}
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import java.net.PasswordAuthentication
import java.net.ProxySelector

actual fun createPlatformHttpClient(proxyConfig: ProxyConfig): HttpClient {
java.net.Authenticator.setDefault(null)

return HttpClient(OkHttp) {
engine {
when (proxyConfig) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,20 +180,6 @@ class AndroidInstaller(
}
}

override fun uninstall(packageName: String) {
Logger.d { "Requesting uninstall for: $packageName" }
val intent = Intent(Intent.ACTION_DELETE).apply {
data = "package:$packageName".toUri()
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(intent)
} catch (e: Exception) {
Logger.w { "Failed to start uninstall for $packageName: ${e.message}" }
}

}

override fun openApp(packageName: String): Boolean {
val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
return if (launchIntent != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.koin.dsl.module
import zed.rainxch.core.data.data_source.TokenStore
import zed.rainxch.core.data.data_source.impl.DefaultTokenStore
Expand All @@ -21,13 +24,16 @@ import zed.rainxch.core.data.repository.FavouritesRepositoryImpl
import zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl
import zed.rainxch.core.data.repository.RateLimitRepositoryImpl
import zed.rainxch.core.data.repository.StarredRepositoryImpl
import zed.rainxch.core.data.repository.ProxyRepositoryImpl
import zed.rainxch.core.data.repository.ThemesRepositoryImpl
import zed.rainxch.core.domain.getPlatform
import zed.rainxch.core.domain.logging.GitHubStoreLogger
import zed.rainxch.core.domain.model.Platform
import zed.rainxch.core.domain.model.ProxyConfig
import zed.rainxch.core.domain.repository.AuthenticationState
import zed.rainxch.core.domain.repository.FavouritesRepository
import zed.rainxch.core.domain.repository.InstalledAppsRepository
import zed.rainxch.core.domain.repository.ProxyRepository
import zed.rainxch.core.domain.repository.RateLimitRepository
import zed.rainxch.core.domain.repository.StarredRepository
import zed.rainxch.core.domain.repository.ThemesRepository
Expand Down Expand Up @@ -84,6 +90,12 @@ val coreModule = module {
)
}

single<ProxyRepository> {
ProxyRepositoryImpl(
preferences = get()
)
}

single<SyncInstalledAppsUseCase> {
SyncInstalledAppsUseCase(
packageMonitor = get(),
Expand All @@ -96,6 +108,32 @@ val coreModule = module {

val networkModule = module {
single<GitHubClientProvider> {
val config = runBlocking {
runCatching {
withTimeout(1_500L) {
get<ProxyRepository>().getProxyConfig().first()
}
}.getOrDefault(ProxyConfig.None)
}

when (config) {
is ProxyConfig.None -> ProxyManager.setNoProxy()
is ProxyConfig.System -> ProxyManager.setSystemProxy()
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
host = config.host,
port = config.port,
username = config.username,
password = config.password
)

is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
host = config.host,
port = config.port,
username = config.username,
password = config.password
)
}

GitHubClientProvider(
tokenStore = get(),
rateLimitRepository = get(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import zed.rainxch.core.data.data_source.TokenStore
Expand All @@ -24,37 +22,36 @@ class GitHubClientProvider(
proxyConfigFlow: StateFlow<ProxyConfig>
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private var currentClient: HttpClient? = null
private val mutex = Mutex()

private val _client: StateFlow<HttpClient> = proxyConfigFlow
.map { proxyConfig ->
mutex.withLock {
currentClient?.close()
val newClient = createGitHubHttpClient(
tokenStore = tokenStore,
rateLimitRepository = rateLimitRepository,
proxyConfig = proxyConfig
)
currentClient = newClient
newClient
@Volatile
private var currentClient: HttpClient = createGitHubHttpClient(
tokenStore = tokenStore,
rateLimitRepository = rateLimitRepository,
proxyConfig = proxyConfigFlow.value
)

init {
proxyConfigFlow
.drop(1)
.distinctUntilChanged()
.onEach { proxyConfig ->
mutex.withLock {
currentClient.close()
currentClient = createGitHubHttpClient(
tokenStore = tokenStore,
rateLimitRepository = rateLimitRepository,
proxyConfig = proxyConfig
)
}
}
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = createGitHubHttpClient(
tokenStore = tokenStore,
rateLimitRepository = rateLimitRepository,
proxyConfig = proxyConfigFlow.value
).also { currentClient = it }
)
.launchIn(scope)
}

/** Get the current HttpClient (always up to date with proxy settings) */
val client: HttpClient get() = _client.value
val client: HttpClient get() = currentClient

fun close() {
currentClient?.close()
currentClient.close()
scope.cancel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package zed.rainxch.core.data.repository

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import zed.rainxch.core.data.network.ProxyManager
import zed.rainxch.core.domain.model.ProxyConfig
import zed.rainxch.core.domain.repository.ProxyRepository

class ProxyRepositoryImpl(
private val preferences: DataStore<Preferences>
) : ProxyRepository {

private val proxyTypeKey = stringPreferencesKey("proxy_type")
private val proxyHostKey = stringPreferencesKey("proxy_host")
private val proxyPortKey = intPreferencesKey("proxy_port")
private val proxyUsernameKey = stringPreferencesKey("proxy_username")
private val proxyPasswordKey = stringPreferencesKey("proxy_password")

override fun getProxyConfig(): Flow<ProxyConfig> {
return preferences.data.map { prefs ->
when (prefs[proxyTypeKey]) {
"system" -> ProxyConfig.System
"http" -> {
val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }
val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }
if (host != null && port != null) {
ProxyConfig.Http(
host = host,
port = port,
username = prefs[proxyUsernameKey],
password = prefs[proxyPasswordKey]
)
} else {
ProxyConfig.None
}
}
"socks" -> {
val host = prefs[proxyHostKey]?.takeIf { it.isNotBlank() }
val port = prefs[proxyPortKey]?.takeIf { it in 1..65535 }
if (host != null && port != null) {
ProxyConfig.Socks(
host = host,
port = port,
username = prefs[proxyUsernameKey],
password = prefs[proxyPasswordKey]
)
} else {
ProxyConfig.None
}
}
else -> ProxyConfig.None
}
}
}

override suspend fun setProxyConfig(config: ProxyConfig) {
// Persist first so config survives crashes, then apply in-memory
preferences.edit { prefs ->
when (config) {
is ProxyConfig.None -> {
prefs[proxyTypeKey] = "none"
prefs.remove(proxyHostKey)
prefs.remove(proxyPortKey)
prefs.remove(proxyUsernameKey)
prefs.remove(proxyPasswordKey)
}
is ProxyConfig.System -> {
prefs[proxyTypeKey] = "system"
prefs.remove(proxyHostKey)
prefs.remove(proxyPortKey)
prefs.remove(proxyUsernameKey)
prefs.remove(proxyPasswordKey)
}
is ProxyConfig.Http -> {
prefs[proxyTypeKey] = "http"
prefs[proxyHostKey] = config.host
prefs[proxyPortKey] = config.port
if (config.username != null) {
prefs[proxyUsernameKey] = config.username!!
} else {
prefs.remove(proxyUsernameKey)
}
if (config.password != null) {
prefs[proxyPasswordKey] = config.password!!
} else {
prefs.remove(proxyPasswordKey)
}
}
is ProxyConfig.Socks -> {
prefs[proxyTypeKey] = "socks"
prefs[proxyHostKey] = config.host
prefs[proxyPortKey] = config.port
if (config.username != null) {
prefs[proxyUsernameKey] = config.username!!
} else {
prefs.remove(proxyUsernameKey)
}
if (config.password != null) {
prefs[proxyPasswordKey] = config.password!!
} else {
prefs.remove(proxyPasswordKey)
}
}
}
}
applyToProxyManager(config)
}

private fun applyToProxyManager(config: ProxyConfig) {
when (config) {
is ProxyConfig.None -> ProxyManager.setNoProxy()
is ProxyConfig.System -> ProxyManager.setSystemProxy()
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
host = config.host,
port = config.port,
username = config.username,
password = config.password
)
is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
host = config.host,
port = config.port,
username = config.username,
password = config.password
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ class DesktopInstaller(

}

override fun uninstall(packageName: String) {
// Desktop doesn't have a unified uninstall mechanism
Logger.d { "Uninstall not supported on desktop for: $packageName" }
}

override fun openApp(packageName: String): Boolean {
// Desktop apps are launched differently per platform
Logger.d { "Open app not supported on desktop for: $packageName" }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package zed.rainxch.core.domain.repository

import kotlinx.coroutines.flow.Flow
import zed.rainxch.core.domain.model.ProxyConfig

interface ProxyRepository {
fun getProxyConfig(): Flow<ProxyConfig>
suspend fun setProxyConfig(config: ProxyConfig)
}
Loading