Skip to content

Commit 8da1532

Browse files
committed
feat(profile): Implement network proxy configuration settings
This commit introduces a new Network section in the profile settings, allowing users to configure HTTP, SOCKS, or system-wide proxy settings. The configuration is persisted using DataStore and applied via a `ProxyManager`. - **feat(profile)**: Added `networkSection` UI component to `ProfileRoot` featuring a `ProxyTypeCard` and a `ProxyDetailsCard` for host, port, and authentication credentials. - **feat(core/domain)**: Introduced `ProxyRepository` interface and `ProxyConfig` models (None, System, HTTP, SOCKS). - **feat(core/data)**: Implemented `ProxyRepositoryImpl` using `DataStore` to persist proxy settings. - **feat(profile)**: Updated `ProfileViewModel` and `ProfileState` to handle proxy actions (type selection, host/port/credential changes, and saving). - **refactor(app)**: Updated `MainViewModel` to initialize and apply the saved proxy configuration on application startup. - **i18n**: Added string resources for proxy types, labels, and status messages. - **chore(di)**: Registered `ProxyRepository` in the dependency injection modules.
1 parent a002a58 commit 8da1532

12 files changed

Lines changed: 545 additions & 6 deletions

File tree

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/MainViewModel.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
77
import kotlinx.coroutines.flow.asStateFlow
88
import kotlinx.coroutines.flow.update
99
import kotlinx.coroutines.launch
10+
import kotlinx.coroutines.flow.first
11+
import zed.rainxch.core.data.network.ProxyManager
12+
import zed.rainxch.core.domain.model.ProxyConfig
1013
import zed.rainxch.core.domain.repository.AuthenticationState
1114
import zed.rainxch.core.domain.repository.InstalledAppsRepository
15+
import zed.rainxch.core.domain.repository.ProxyRepository
1216
import zed.rainxch.core.domain.repository.RateLimitRepository
1317
import zed.rainxch.core.domain.repository.ThemesRepository
1418
import zed.rainxch.core.domain.use_cases.SyncInstalledAppsUseCase
@@ -18,7 +22,8 @@ class MainViewModel(
1822
private val installedAppsRepository: InstalledAppsRepository,
1923
private val authenticationState: AuthenticationState,
2024
private val rateLimitRepository: RateLimitRepository,
21-
private val syncUseCase: SyncInstalledAppsUseCase
25+
private val syncUseCase: SyncInstalledAppsUseCase,
26+
private val proxyRepository: ProxyRepository
2227
) : ViewModel() {
2328

2429
private val _state = MutableStateFlow(MainState())
@@ -94,6 +99,26 @@ class MainViewModel(
9499
installedAppsRepository.checkAllForUpdates()
95100
}
96101
}
102+
103+
viewModelScope.launch(Dispatchers.IO) {
104+
val savedConfig = proxyRepository.getProxyConfig().first()
105+
when (savedConfig) {
106+
is ProxyConfig.None -> ProxyManager.setNoProxy()
107+
is ProxyConfig.System -> ProxyManager.setSystemProxy()
108+
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
109+
host = savedConfig.host,
110+
port = savedConfig.port,
111+
username = savedConfig.username,
112+
password = savedConfig.password
113+
)
114+
is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
115+
host = savedConfig.host,
116+
port = savedConfig.port,
117+
username = savedConfig.username,
118+
password = savedConfig.password
119+
)
120+
}
121+
}
97122
}
98123

99124
fun onAction(action: MainAction) {

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/SharedModules.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ val mainModule: Module = module {
1515
installedAppsRepository = get(),
1616
rateLimitRepository = get(),
1717
syncUseCase = get(),
18-
authenticationState = get()
18+
authenticationState = get(),
19+
proxyRepository = get()
1920
)
2021
}
2122
}

core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ import zed.rainxch.core.data.repository.FavouritesRepositoryImpl
2121
import zed.rainxch.core.data.repository.InstalledAppsRepositoryImpl
2222
import zed.rainxch.core.data.repository.RateLimitRepositoryImpl
2323
import zed.rainxch.core.data.repository.StarredRepositoryImpl
24+
import zed.rainxch.core.data.repository.ProxyRepositoryImpl
2425
import zed.rainxch.core.data.repository.ThemesRepositoryImpl
2526
import zed.rainxch.core.domain.getPlatform
2627
import zed.rainxch.core.domain.logging.GitHubStoreLogger
2728
import zed.rainxch.core.domain.model.Platform
2829
import zed.rainxch.core.domain.repository.AuthenticationState
2930
import zed.rainxch.core.domain.repository.FavouritesRepository
3031
import zed.rainxch.core.domain.repository.InstalledAppsRepository
32+
import zed.rainxch.core.domain.repository.ProxyRepository
3133
import zed.rainxch.core.domain.repository.RateLimitRepository
3234
import zed.rainxch.core.domain.repository.StarredRepository
3335
import zed.rainxch.core.domain.repository.ThemesRepository
@@ -84,6 +86,12 @@ val coreModule = module {
8486
)
8587
}
8688

89+
single<ProxyRepository> {
90+
ProxyRepositoryImpl(
91+
preferences = get()
92+
)
93+
}
94+
8795
single<SyncInstalledAppsUseCase> {
8896
SyncInstalledAppsUseCase(
8997
packageMonitor = get(),
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package zed.rainxch.core.data.repository
2+
3+
import androidx.datastore.core.DataStore
4+
import androidx.datastore.preferences.core.Preferences
5+
import androidx.datastore.preferences.core.edit
6+
import androidx.datastore.preferences.core.intPreferencesKey
7+
import androidx.datastore.preferences.core.stringPreferencesKey
8+
import kotlinx.coroutines.flow.Flow
9+
import kotlinx.coroutines.flow.map
10+
import zed.rainxch.core.data.network.ProxyManager
11+
import zed.rainxch.core.domain.model.ProxyConfig
12+
import zed.rainxch.core.domain.repository.ProxyRepository
13+
14+
class ProxyRepositoryImpl(
15+
private val preferences: DataStore<Preferences>
16+
) : ProxyRepository {
17+
18+
private val proxyTypeKey = stringPreferencesKey("proxy_type")
19+
private val proxyHostKey = stringPreferencesKey("proxy_host")
20+
private val proxyPortKey = intPreferencesKey("proxy_port")
21+
private val proxyUsernameKey = stringPreferencesKey("proxy_username")
22+
private val proxyPasswordKey = stringPreferencesKey("proxy_password")
23+
24+
override fun getProxyConfig(): Flow<ProxyConfig> {
25+
return preferences.data.map { prefs ->
26+
when (prefs[proxyTypeKey]) {
27+
"system" -> ProxyConfig.System
28+
"http" -> ProxyConfig.Http(
29+
host = prefs[proxyHostKey] ?: "",
30+
port = prefs[proxyPortKey] ?: 8080,
31+
username = prefs[proxyUsernameKey],
32+
password = prefs[proxyPasswordKey]
33+
)
34+
"socks" -> ProxyConfig.Socks(
35+
host = prefs[proxyHostKey] ?: "",
36+
port = prefs[proxyPortKey] ?: 1080,
37+
username = prefs[proxyUsernameKey],
38+
password = prefs[proxyPasswordKey]
39+
)
40+
else -> ProxyConfig.None
41+
}
42+
}
43+
}
44+
45+
override suspend fun setProxyConfig(config: ProxyConfig) {
46+
applyToProxyManager(config)
47+
preferences.edit { prefs ->
48+
when (config) {
49+
is ProxyConfig.None -> {
50+
prefs[proxyTypeKey] = "none"
51+
prefs.remove(proxyHostKey)
52+
prefs.remove(proxyPortKey)
53+
prefs.remove(proxyUsernameKey)
54+
prefs.remove(proxyPasswordKey)
55+
}
56+
is ProxyConfig.System -> {
57+
prefs[proxyTypeKey] = "system"
58+
prefs.remove(proxyHostKey)
59+
prefs.remove(proxyPortKey)
60+
prefs.remove(proxyUsernameKey)
61+
prefs.remove(proxyPasswordKey)
62+
}
63+
is ProxyConfig.Http -> {
64+
prefs[proxyTypeKey] = "http"
65+
prefs[proxyHostKey] = config.host
66+
prefs[proxyPortKey] = config.port
67+
if (config.username != null) {
68+
prefs[proxyUsernameKey] = config.username!!
69+
} else {
70+
prefs.remove(proxyUsernameKey)
71+
}
72+
if (config.password != null) {
73+
prefs[proxyPasswordKey] = config.password!!
74+
} else {
75+
prefs.remove(proxyPasswordKey)
76+
}
77+
}
78+
is ProxyConfig.Socks -> {
79+
prefs[proxyTypeKey] = "socks"
80+
prefs[proxyHostKey] = config.host
81+
prefs[proxyPortKey] = config.port
82+
if (config.username != null) {
83+
prefs[proxyUsernameKey] = config.username!!
84+
} else {
85+
prefs.remove(proxyUsernameKey)
86+
}
87+
if (config.password != null) {
88+
prefs[proxyPasswordKey] = config.password!!
89+
} else {
90+
prefs.remove(proxyPasswordKey)
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
private fun applyToProxyManager(config: ProxyConfig) {
98+
when (config) {
99+
is ProxyConfig.None -> ProxyManager.setNoProxy()
100+
is ProxyConfig.System -> ProxyManager.setSystemProxy()
101+
is ProxyConfig.Http -> ProxyManager.setHttpProxy(
102+
host = config.host,
103+
port = config.port,
104+
username = config.username,
105+
password = config.password
106+
)
107+
is ProxyConfig.Socks -> ProxyManager.setSocksProxy(
108+
host = config.host,
109+
port = config.port,
110+
username = config.username,
111+
password = config.password
112+
)
113+
}
114+
}
115+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package zed.rainxch.core.domain.repository
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import zed.rainxch.core.domain.model.ProxyConfig
5+
6+
interface ProxyRepository {
7+
fun getProxyConfig(): Flow<ProxyConfig>
8+
suspend fun setProxyConfig(config: ProxyConfig)
9+
}

core/presentation/src/commonMain/composeResources/values/strings.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@
108108

109109
<!-- Sections -->
110110
<string name="section_appearance">APPEARANCE</string>
111+
<string name="section_network">NETWORK</string>
111112
<string name="section_about">ABOUT</string>
112113

113114
<!-- Appearance -->
@@ -123,6 +124,19 @@
123124
<!-- Account -->
124125
<string name="logout">Logout</string>
125126

127+
<!-- Proxy -->
128+
<string name="proxy_type">Proxy Type</string>
129+
<string name="proxy_none">None</string>
130+
<string name="proxy_system">System</string>
131+
<string name="proxy_http">HTTP</string>
132+
<string name="proxy_socks">SOCKS</string>
133+
<string name="proxy_host">Host</string>
134+
<string name="proxy_port">Port</string>
135+
<string name="proxy_username">Username (optional)</string>
136+
<string name="proxy_password">Password (optional)</string>
137+
<string name="proxy_save">Save</string>
138+
<string name="proxy_saved">Proxy settings saved</string>
139+
126140
<!-- Snackbar -->
127141
<string name="logout_success">Logged out successfully, redirecting...</string>
128142

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,10 @@ sealed interface ProfileAction {
1313
data object OnLogoutDismiss : ProfileAction
1414
data object OnHelpClick : ProfileAction
1515
data class OnFontThemeSelected(val fontTheme: FontTheme) : ProfileAction
16+
data class OnProxyTypeSelected(val type: ProxyType) : ProfileAction
17+
data class OnProxyHostChanged(val host: String) : ProfileAction
18+
data class OnProxyPortChanged(val port: String) : ProfileAction
19+
data class OnProxyUsernameChanged(val username: String) : ProfileAction
20+
data class OnProxyPasswordChanged(val password: String) : ProfileAction
21+
data object OnProxySave : ProfileAction
1622
}

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ package zed.rainxch.profile.presentation
33
sealed interface ProfileEvent {
44
data object OnLogoutSuccessful : ProfileEvent
55
data class OnLogoutError(val message: String) : ProfileEvent
6+
data object OnProxySaved : ProfileEvent
67
}

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import zed.rainxch.core.presentation.utils.ObserveAsEvents
4040
import zed.rainxch.profile.presentation.components.LogoutDialog
4141
import zed.rainxch.profile.presentation.components.sections.about
4242
import zed.rainxch.profile.presentation.components.sections.logout
43+
import zed.rainxch.profile.presentation.components.sections.networkSection
4344
import zed.rainxch.profile.presentation.components.sections.profile
4445
import zed.rainxch.profile.presentation.components.sections.settings
4546

@@ -67,6 +68,12 @@ fun ProfileRoot(
6768
snackbarState.showSnackbar(event.message)
6869
}
6970
}
71+
72+
ProfileEvent.OnProxySaved -> {
73+
coroutineScope.launch {
74+
snackbarState.showSnackbar(getString(Res.string.proxy_saved))
75+
}
76+
}
7077
}
7178
}
7279

@@ -140,6 +147,15 @@ fun ProfileScreen(
140147
Spacer(Modifier.height(16.dp))
141148
}
142149

150+
networkSection(
151+
state = state,
152+
onAction = onAction
153+
)
154+
155+
item {
156+
Spacer(Modifier.height(16.dp))
157+
}
158+
143159
about(
144160
versionName = state.versionName,
145161
onAction = onAction

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package zed.rainxch.profile.presentation
22

33
import zed.rainxch.core.domain.model.AppTheme
44
import zed.rainxch.core.domain.model.FontTheme
5+
import zed.rainxch.core.domain.model.ProxyConfig
56
import zed.rainxch.profile.domain.model.UserProfile
67

78
data class ProfileState(
@@ -12,5 +13,23 @@ data class ProfileState(
1213
val isUserLoggedIn: Boolean = false,
1314
val isAmoledThemeEnabled: Boolean = false,
1415
val isDarkTheme: Boolean? = null,
15-
val versionName: String = ""
16-
)
16+
val versionName: String = "",
17+
val proxyType: ProxyType = ProxyType.NONE,
18+
val proxyHost: String = "",
19+
val proxyPort: String = "",
20+
val proxyUsername: String = "",
21+
val proxyPassword: String = "",
22+
)
23+
24+
enum class ProxyType {
25+
NONE, SYSTEM, HTTP, SOCKS;
26+
27+
companion object {
28+
fun fromConfig(config: ProxyConfig): ProxyType = when (config) {
29+
is ProxyConfig.None -> NONE
30+
is ProxyConfig.System -> SYSTEM
31+
is ProxyConfig.Http -> HTTP
32+
is ProxyConfig.Socks -> SOCKS
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)