diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml
index 2c6cc402..9c3aeb7d 100644
--- a/composeApp/src/androidMain/AndroidManifest.xml
+++ b/composeApp/src/androidMain/AndroidManifest.xml
@@ -11,7 +11,6 @@
-
+ bottomNavigationHeight = with(density) { coordinates.size.height.toDp() }
+ }
)
}
}
diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt
index e4a59a29..7ad21621 100644
--- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt
+++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.android.kt
@@ -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) {
diff --git a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt
index ef2c3345..55e686ed 100644
--- a/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt
+++ b/core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidInstaller.kt
@@ -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) {
diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
index 4f2d957b..40f778eb 100644
--- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
+++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
@@ -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
@@ -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
@@ -84,6 +90,12 @@ val coreModule = module {
)
}
+ single {
+ ProxyRepositoryImpl(
+ preferences = get()
+ )
+ }
+
single {
SyncInstalledAppsUseCase(
packageMonitor = get(),
@@ -96,6 +108,32 @@ val coreModule = module {
val networkModule = module {
single {
+ val config = runBlocking {
+ runCatching {
+ withTimeout(1_500L) {
+ get().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(),
diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt
index f0afb0c9..ef9ce955 100644
--- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt
+++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt
@@ -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
@@ -24,37 +22,36 @@ class GitHubClientProvider(
proxyConfigFlow: StateFlow
) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
- private var currentClient: HttpClient? = null
private val mutex = Mutex()
- private val _client: StateFlow = 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()
}
}
\ No newline at end of file
diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt
new file mode 100644
index 00000000..1531dca8
--- /dev/null
+++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt
@@ -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
+) : 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 {
+ 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
+ )
+ }
+ }
+}
diff --git a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt
index d2e910c1..164eef17 100644
--- a/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt
+++ b/core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt
@@ -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" }
diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt
new file mode 100644
index 00000000..0d8b710f
--- /dev/null
+++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt
@@ -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
+ suspend fun setProxyConfig(config: ProxyConfig)
+}
diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt
index ceee0a8d..2fa394a0 100644
--- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt
+++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/system/Installer.kt
@@ -28,7 +28,5 @@ interface Installer {
fun getApkInfoExtractor(): InstallerInfoExtractor
- fun uninstall(packageName: String)
-
fun openApp(packageName: String): Boolean
}
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
index bc09ee53..85d28bc1 100644
--- a/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
+++ b/core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
@@ -109,6 +109,7 @@
চেহারা
সম্পর্কে
+ নেটওয়ার্ক
থিমের রঙ
@@ -349,15 +350,11 @@
ইনস্টল মুলতুবি
-
- আনইনস্টল
+
খুলুন
- ডাউনগ্রেডের জন্য আনইনস্টল প্রয়োজন
- সংস্করণ %1$s ইনস্টল করতে বর্তমান সংস্করণ (%2$s) প্রথমে আনইনস্টল করতে হবে। অ্যাপের ডেটা মুছে যাবে।
- প্রথমে আনইনস্টল করুন
+ সংস্করণ %1$s ইনস্টল করা যাচ্ছে না কারণ একটি নতুন সংস্করণ (%2$s) ইতিমধ্যে ইনস্টল করা আছে। অনুগ্রহ করে প্রথমে বর্তমান সংস্করণটি ম্যানুয়ালি আনইনস্টল করুন।
%1$s ইনস্টল করুন
%1$s খুলতে ব্যর্থ
- %1$s আনইনস্টল করতে ব্যর্থ
সর্বশেষ
@@ -370,4 +367,25 @@
%1$d ঘণ্টা আগে
আপডেট পরীক্ষা করা হচ্ছে…
+
+ প্রক্সি ধরন
+ নেই
+ সিস্টেম
+ HTTP
+ SOCKS
+ হোস্ট
+ পোর্ট
+ ব্যবহারকারীর নাম (ঐচ্ছিক)
+ পাসওয়ার্ড (ঐচ্ছিক)
+ প্রক্সি সংরক্ষণ
+ প্রক্সি সেটিংস সংরক্ষিত হয়েছে
+ আপনার ডিভাইসের প্রক্সি সেটিংস ব্যবহার করে
+ পোর্ট ১–৬৫৫৩৫ এর মধ্যে হতে হবে
+ সরাসরি সংযোগ, কোনো প্রক্সি নেই
+ প্রক্সি সেটিংস সংরক্ষণ করতে ব্যর্থ হয়েছে
+ প্রক্সি হোস্ট প্রয়োজন
+ অবৈধ প্রক্সি পোর্ট
+ পাসওয়ার্ড দেখান
+ পাসওয়ার্ড লুকান
+
diff --git a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
index 18760f52..1e85c169 100644
--- a/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
+++ b/core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
@@ -91,6 +91,7 @@
APARIENCIA
ACERCA DE
+ RED
Color del tema
Tema negro AMOLED
@@ -314,15 +315,11 @@
Inspeccionar con AppManager
Verificar permisos, rastreadores y seguridad
-
- Desinstalar
+
Abrir
- La degradación requiere desinstalar
- Instalar la versión %1$s requiere desinstalar la versión actual (%2$s) primero. Los datos de la app se perderán.
- Desinstalar primero
+ No se puede instalar la versión %1$s porque ya hay una versión más reciente (%2$s) instalada. Por favor, desinstala la versión actual manualmente primero.
Instalar %1$s
Error al abrir %1$s
- Error al desinstalar %1$s
Última
@@ -335,4 +332,25 @@
hace %1$d h
Comprobando actualizaciones…
+
+ Tipo de proxy
+ Ninguno
+ Sistema
+ HTTP
+ SOCKS
+ Host
+ Puerto
+ Nombre de usuario (opcional)
+ Contraseña (opcional)
+ Guardar proxy
+ Configuración de proxy guardada
+ Usa la configuración de proxy del dispositivo
+ El puerto debe ser 1–65535
+ Conexión directa, sin proxy
+ No se pudieron guardar los ajustes del proxy
+ Se requiere el host del proxy
+ Puerto de proxy no válido
+ Mostrar contraseña
+ Ocultar contraseña
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
index bdd14de5..09330e25 100644
--- a/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
+++ b/core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
@@ -91,6 +91,7 @@
APPARENCE
À PROPOS
+ RÉSEAU
Couleur du thème
Thème noir AMOLED
@@ -314,15 +315,11 @@
Inspecter avec AppManager
Vérifier les permissions, trackers et sécurité
-
- Désinstaller
+
Ouvrir
- La rétrogradation nécessite la désinstallation
- L\'installation de la version %1$s nécessite la désinstallation de la version actuelle (%2$s). Les données de l\'application seront perdues.
- Désinstaller d\'abord
+ Impossible d\'installer la version %1$s car une version plus récente (%2$s) est déjà installée. Veuillez d\'abord désinstaller manuellement la version actuelle.
Installer %1$s
Impossible d\'ouvrir %1$s
- Impossible de désinstaller %1$s
Dernière
@@ -335,4 +332,25 @@
il y a %1$d h
Vérification des mises à jour…
+
+ Type de proxy
+ Aucun
+ Système
+ HTTP
+ SOCKS
+ Hôte
+ Port
+ Nom d\'utilisateur (facultatif)
+ Mot de passe (facultatif)
+ Sauvegarder le Proxy
+ Paramètres du proxy enregistrés
+ Utilise les paramètres proxy de l\'appareil
+ Le port doit être entre 1 et 65535
+ Connexion directe, pas de proxy
+ Échec de l'enregistrement des paramètres du proxy
+ L’hôte du proxy est requis
+ Port proxy invalide
+ Afficher le mot de passe
+ Masquer le mot de passe
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
index 9a8d3b05..d45230c5 100644
--- a/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
+++ b/core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
@@ -109,6 +109,7 @@
उपस्थिति
के बारे में
+ नेटवर्क
थीम रंग
@@ -349,15 +350,11 @@
इंस्टॉल लंबित
-
- अनइंस्टॉल
+
खोलें
- डाउनग्रेड के लिए अनइंस्टॉल आवश्यक
- संस्करण %1$s इंस्टॉल करने के लिए पहले वर्तमान संस्करण (%2$s) को अनइंस्टॉल करना होगा। ऐप डेटा खो जाएगा।
- पहले अनइंस्टॉल करें
+ संस्करण %1$s इंस्टॉल नहीं किया जा सकता क्योंकि एक नया संस्करण (%2$s) पहले से इंस्टॉल है। कृपया पहले वर्तमान संस्करण को मैन्युअल रूप से अनइंस्टॉल करें।
%1$s इंस्टॉल करें
%1$s खोलने में विफल
- %1$s अनइंस्टॉल करने में विफल
नवीनतम
@@ -369,4 +366,26 @@
%1$d मिनट पहले
%1$d घंटे पहले
अपडेट की जाँच हो रही है…
+
+
+ प्रॉक्सी प्रकार
+ कोई नहीं
+ सिस्टम
+ HTTP
+ SOCKS
+ होस्ट
+ पोर्ट
+ उपयोगकर्ता नाम (वैकल्पिक)
+ पासवर्ड (वैकल्पिक)
+ प्रॉक्सी सहेजें
+ प्रॉक्सी सेटिंग्स सहेजी गईं
+ आपके डिवाइस की प्रॉक्सी सेटिंग का उपयोग करता है
+ पोर्ट 1–65535 के बीच होना चाहिए
+ सीधा कनेक्शन, कोई प्रॉक्सी नहीं
+ प्रॉक्सी सेटिंग्स सहेजने में विफल
+ प्रॉक्सी होस्ट आवश्यक है
+ अमान्य प्रॉक्सी पोर्ट
+ पासवर्ड दिखाएँ
+ पासवर्ड छुपाएँ
+
diff --git a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
index 96576292..88381c51 100644
--- a/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
+++ b/core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
@@ -109,6 +109,7 @@
ASPETTO
INFORMAZIONI
+ RETE
Colore del Tema
@@ -345,15 +346,11 @@
Installazione in sospeso
-
- Disinstalla
+
Apri
- Il downgrade richiede la disinstallazione
- L\'installazione della versione %1$s richiede la disinstallazione della versione corrente (%2$s). I dati dell\'app verranno persi.
- Disinstalla prima
+ Impossibile installare la versione %1$s perché una versione più recente (%2$s) è già installata. Disinstalla manualmente la versione corrente prima di procedere.
Installa %1$s
Impossibile aprire %1$s
- Impossibile disinstallare %1$s
Ultima
@@ -371,4 +368,25 @@
%1$d h fa
Controllo aggiornamenti…
+
+ Tipo di proxy
+ Nessuno
+ Sistema
+ HTTP
+ SOCKS
+ Host
+ Porta
+ Nome utente (facoltativo)
+ Password (facoltativo)
+ Salva Proxy
+ Impostazioni proxy salvate
+ Usa le impostazioni proxy del dispositivo
+ La porta deve essere 1–65535
+ Connessione diretta, nessun proxy
+ Impossibile salvare le impostazioni del proxy
+ L'host del proxy è obbligatorio
+ Porta proxy non valida
+ Mostra password
+ Nascondi password
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
index 9bfa17a0..2d14564e 100644
--- a/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
+++ b/core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
@@ -91,6 +91,7 @@
外観
情報
+ ネットワーク
テーマカラー
AMOLED ブラックテーマ
@@ -314,15 +315,11 @@
AppManagerで検査
権限、トラッカー、セキュリティを確認
-
- アンインストール
+
開く
- ダウングレードにはアンインストールが必要
- バージョン%1$sのインストールには、現在のバージョン(%2$s)のアンインストールが必要です。アプリデータは失われます。
- 先にアンインストール
+ より新しいバージョン(%2$s)がすでにインストールされているため、バージョン%1$sをインストールできません。まず現在のバージョンを手動でアンインストールしてください。
%1$sをインストール
%1$sを開けませんでした
- %1$sのアンインストールに失敗しました
最新
@@ -335,4 +332,25 @@
%1$d時間前
アップデートを確認中…
+
+ プロキシの種類
+ なし
+ システム
+ HTTP
+ SOCKS
+ ホスト
+ ポート
+ ユーザー名(任意)
+ パスワード(任意)
+ プロキシを保存
+ プロキシ設定を保存しました
+ デバイスのプロキシ設定を使用します
+ ポートは1〜65535の範囲で指定してください
+ 直接接続、プロキシなし
+ プロキシ設定の保存に失敗しました
+ プロキシホストは必須です
+ 無効なプロキシポート
+ パスワードを表示
+ パスワードを非表示
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-kr/strings-kr.xml b/core/presentation/src/commonMain/composeResources/values-kr/strings-kr.xml
index a7fbad23..ee6e7872 100644
--- a/core/presentation/src/commonMain/composeResources/values-kr/strings-kr.xml
+++ b/core/presentation/src/commonMain/composeResources/values-kr/strings-kr.xml
@@ -107,6 +107,7 @@
외관
정보
+ 네트워크
테마 색상
@@ -347,15 +348,11 @@
설치 대기 중
-
- 제거
+
열기
- 다운그레이드를 위해 제거가 필요합니다
- 버전 %1$s을(를) 설치하려면 현재 버전(%2$s)을 먼저 제거해야 합니다. 앱 데이터가 삭제됩니다.
- 먼저 제거
+ 더 최신 버전(%2$s)이 이미 설치되어 있어 버전 %1$s을(를) 설치할 수 없습니다. 현재 버전을 먼저 수동으로 제거해 주세요.
%1$s 설치
%1$s 열기 실패
- %1$s 제거 실패
최신
@@ -368,4 +365,25 @@
%1$d시간 전
업데이트 확인 중…
+
+ 프록시 유형
+ 없음
+ 시스템
+ HTTP
+ SOCKS
+ 호스트
+ 포트
+ 사용자 이름 (선택 사항)
+ 비밀번호 (선택 사항)
+ 프록시 저장
+ 프록시 설정이 저장되었습니다
+ 기기의 프록시 설정을 사용합니다
+ 포트는 1–65535 사이여야 합니다
+ 직접 연결, 프록시 없음
+ 프록시 설정을 저장하지 못했습니다
+ 프록시 호스트가 필요합니다
+ 잘못된 프록시 포트
+ 비밀번호 표시
+ 비밀번호 숨기기
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
index 68802d1b..d266bd52 100644
--- a/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
+++ b/core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
@@ -92,6 +92,7 @@
WYGLĄD
O APLIKACJI
+ SIEĆ
Kolor motywu
Motyw AMOLED Black
@@ -312,15 +313,11 @@
Oczekuje na instalację
-
- Odinstaluj
+
Otwórz
- Obniżenie wersji wymaga odinstalowania
- Instalacja wersji %1$s wymaga odinstalowania bieżącej wersji (%2$s). Dane aplikacji zostaną utracone.
- Najpierw odinstaluj
+ Nie można zainstalować wersji %1$s, ponieważ nowsza wersja (%2$s) jest już zainstalowana. Najpierw ręcznie odinstaluj bieżącą wersję.
Zainstaluj %1$s
Nie udało się otworzyć %1$s
- Nie udało się odinstalować %1$s
Najnowsza
@@ -333,4 +330,25 @@
%1$d godz. temu
Sprawdzanie aktualizacji…
+
+ Typ proxy
+ Brak
+ Systemowy
+ HTTP
+ SOCKS
+ Host
+ Port
+ Nazwa użytkownika (opcjonalnie)
+ Hasło (opcjonalnie)
+ Zapisz Proxy
+ Ustawienia proxy zostały zapisane
+ Używa ustawień proxy urządzenia
+ Port musi być z zakresu 1–65535
+ Połączenie bezpośrednie, bez proxy
+ Nie udało się zapisać ustawień proxy
+ Host proxy jest wymagany
+ Nieprawidłowy port proxy
+ Pokaż hasło
+ Ukryj hasło
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
index 024ecb30..f74c0baa 100644
--- a/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
+++ b/core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
@@ -91,6 +91,7 @@
ВНЕШНИЙ ВИД
О ПРИЛОЖЕНИИ
+ СЕТЬ
Цвет темы
AMOLED чёрная тема
@@ -314,15 +315,11 @@
Ожидает установки
-
- Удалить
+
Открыть
- Для понижения версии требуется удаление
- Для установки версии %1$s необходимо сначала удалить текущую версию (%2$s). Данные приложения будут потеряны.
- Сначала удалить
+ Невозможно установить версию %1$s, так как более новая версия (%2$s) уже установлена. Пожалуйста, сначала удалите текущую версию вручную.
Установить %1$s
Не удалось открыть %1$s
- Не удалось удалить %1$s
Последняя
@@ -335,4 +332,25 @@
%1$d ч назад
Проверка обновлений…
+
+ Тип прокси
+ Нет
+ Системный
+ HTTP
+ SOCKS
+ Хост
+ Порт
+ Имя пользователя (необязательно)
+ Пароль (необязательно)
+ Сохранить прокси
+ Настройки прокси сохранены
+ Использует прокси-настройки устройства
+ Порт должен быть 1–65535
+ Прямое подключение, без прокси
+ Не удалось сохранить настройки прокси
+ Требуется хост прокси
+ Недопустимый порт прокси
+ Показать пароль
+ Скрыть пароль
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
index 59274cd8..655a4645 100644
--- a/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
+++ b/core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
@@ -108,6 +108,7 @@
GÖRÜNÜM
HAKKINDA
+ AĞ
Tema Rengi
@@ -346,15 +347,11 @@
Kurulum bekleniyor
-
- Kaldır
+
Aç
- Sürüm düşürme kaldırma gerektirir
- %1$s sürümünü yüklemek için önce mevcut sürümü (%2$s) kaldırmanız gerekir. Uygulama verileri kaybolacaktır.
- Önce kaldır
+ Daha yeni bir sürüm (%2$s) zaten yüklü olduğu için %1$s sürümü yüklenemiyor. Lütfen önce mevcut sürümü manuel olarak kaldırın.
%1$s yükle
%1$s açılamadı
- %1$s kaldırılamadı
En son
@@ -367,4 +364,25 @@
%1$d sa önce
Güncellemeler kontrol ediliyor…
+
+ Proxy Türü
+ Yok
+ Sistem
+ HTTP
+ SOCKS
+ Ana Bilgisayar
+ Port
+ Kullanıcı adı (isteğe bağlı)
+ Şifre (isteğe bağlı)
+ Proxy'yi Kaydet
+ Proxy ayarları kaydedildi
+ Cihazınızın proxy ayarlarını kullanır
+ Port 1–65535 arası olmalı
+ Doğrudan bağlantı, proxy yok
+ Proxy ayarları kaydedilemedi
+ Proxy ana bilgisayarı gerekli
+ Geçersiz proxy portu
+ Şifreyi göster
+ Şifreyi gizle
+
diff --git a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
index 04b000ad..75f9027b 100644
--- a/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
+++ b/core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
@@ -93,6 +93,7 @@
外观
关于
+ 网络
主题颜色
AMOLED 黑色主题
@@ -315,15 +316,11 @@
使用 AppManager 检查
检查权限、追踪器和安全性
-
- 卸载
+
打开
- 降级需要先卸载
- 安装版本 %1$s 需要先卸载当前版本(%2$s)。应用数据将丢失。
- 先卸载
+ 无法安装版本 %1$s,因为已安装了更新的版本(%2$s)。请先手动卸载当前版本。
安装 %1$s
无法打开 %1$s
- 无法卸载 %1$s
最新
@@ -336,4 +333,25 @@
%1$d 小时前
正在检查更新…
+
+ 代理类型
+ 无
+ 系统代理
+ HTTP
+ SOCKS
+ 主机地址
+ 端口
+ 用户名(可选)
+ 密码(可选)
+ 保存代理
+ 代理设置已保存
+ 使用设备的代理设置
+ 端口必须为 1–65535
+ 直连,不使用代理
+ 无法保存代理设置
+ 必须填写代理主机
+ 无效的代理端口
+ 显示密码
+ 隐藏密码
+
\ No newline at end of file
diff --git a/core/presentation/src/commonMain/composeResources/values/strings.xml b/core/presentation/src/commonMain/composeResources/values/strings.xml
index 7f90bf06..71f968e3 100644
--- a/core/presentation/src/commonMain/composeResources/values/strings.xml
+++ b/core/presentation/src/commonMain/composeResources/values/strings.xml
@@ -108,6 +108,7 @@
APPEARANCE
+ NETWORK
ABOUT
@@ -123,6 +124,27 @@
Logout
+
+ Proxy Type
+ None
+ System
+ HTTP
+ SOCKS
+ Host
+ Port
+ Username (optional)
+ Password (optional)
+ Save Proxy
+ Proxy settings saved
+ Uses your device's proxy settings
+ Port must be 1–65535
+ Direct connection, no proxy
+ Failed to save proxy settings
+ Proxy host is required
+ Invalid proxy port
+ Show password
+ Hide password
+
Logged out successfully, redirecting...
@@ -171,15 +193,11 @@
Installing
Pending install
-
- Uninstall
+
Open
- Downgrade requires uninstall
- Installing version %1$s requires uninstalling the current version (%2$s) first. Your app data will be lost.
- Uninstall first
+ Cannot install version %1$s because a newer version (%2$s) is already installed. Please uninstall the current version manually first.
Install %1$s
Failed to open %1$s
- Failed to uninstall %1$s
Open in Obtainium
diff --git a/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalBottomNavigationHeight.kt b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalBottomNavigationHeight.kt
new file mode 100644
index 00000000..6861e495
--- /dev/null
+++ b/core/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/locals/LocalBottomNavigationHeight.kt
@@ -0,0 +1,8 @@
+package zed.rainxch.core.presentation.locals
+
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.unit.Dp
+
+val LocalBottomNavigationHeight = compositionLocalOf {
+ error("Not initialized yet")
+}
\ No newline at end of file
diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt
index 73327800..1e25b4ce 100644
--- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt
+++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsAction.kt
@@ -12,6 +12,5 @@ sealed interface AppsAction {
data object OnCancelUpdateAll : AppsAction
data object OnCheckAllForUpdates : AppsAction
data object OnRefresh : AppsAction
- data class OnUninstallApp(val app: InstalledApp) : AppsAction
data class OnNavigateToRepo(val repoId: Long) : AppsAction
}
\ No newline at end of file
diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt
index 1c0ba136..46ca4691 100644
--- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt
+++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt
@@ -27,7 +27,6 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Update
-import androidx.compose.material.icons.outlined.DeleteOutline
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
@@ -302,7 +301,6 @@ fun AppsScreen(
onOpenClick = { onAction(AppsAction.OnOpenApp(appItem.installedApp)) },
onUpdateClick = { onAction(AppsAction.OnUpdateApp(appItem.installedApp)) },
onCancelClick = { onAction(AppsAction.OnCancelUpdate(appItem.installedApp.packageName)) },
- onUninstallClick = { onAction(AppsAction.OnUninstallApp(appItem.installedApp)) },
onRepoClick = { onAction(AppsAction.OnNavigateToRepo(appItem.installedApp.repoId)) },
modifier = Modifier.liquefiable(liquidState)
)
@@ -376,7 +374,6 @@ fun AppItemCard(
onOpenClick: () -> Unit,
onUpdateClick: () -> Unit,
onCancelClick: () -> Unit,
- onUninstallClick: () -> Unit,
onRepoClick: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -556,23 +553,6 @@ fun AppItemCard(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
- // Uninstall icon button (shown when not pending and not actively updating)
- if (!app.isPendingInstall &&
- appItem.updateState !is UpdateState.Downloading &&
- appItem.updateState !is UpdateState.Installing &&
- appItem.updateState !is UpdateState.CheckingUpdate
- ) {
- IconButton(
- onClick = onUninstallClick
- ) {
- Icon(
- imageVector = Icons.Outlined.DeleteOutline,
- contentDescription = stringResource(Res.string.uninstall),
- tint = MaterialTheme.colorScheme.error
- )
- }
- }
-
Button(
onClick = onOpenClick,
modifier = Modifier.weight(1f),
diff --git a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
index 00c92a66..c3ceba56 100644
--- a/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
+++ b/feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
@@ -188,10 +188,6 @@ class AppsViewModel(
refresh()
}
- is AppsAction.OnUninstallApp -> {
- uninstallApp(action.app)
- }
-
is AppsAction.OnNavigateToRepo -> {
viewModelScope.launch {
_events.send(AppsEvent.NavigateToRepo(action.repoId))
@@ -200,22 +196,6 @@ class AppsViewModel(
}
}
- private fun uninstallApp(app: InstalledApp) {
- viewModelScope.launch {
- try {
- installer.uninstall(app.packageName)
- logger.debug("Requested uninstall for ${app.packageName}")
- } catch (e: Exception) {
- logger.error("Failed to request uninstall for ${app.packageName}: ${e.message}")
- _events.send(
- AppsEvent.ShowError(
- getString(Res.string.failed_to_uninstall, app.appName)
- )
- )
- }
- }
- }
-
private fun openApp(app: InstalledApp) {
viewModelScope.launch {
try {
diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt
index 10064610..cf32d2c1 100644
--- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt
+++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsAction.kt
@@ -28,7 +28,6 @@ sealed interface DetailsAction {
data object OnToggleFavorite : DetailsAction
data object CheckForUpdates : DetailsAction
data object UpdateApp : DetailsAction
- data object UninstallApp : DetailsAction
data object OpenApp : DetailsAction
data class OnMessage(val messageText: StringResource) : DetailsAction
diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt
index e940dfd4..c559f81a 100644
--- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt
+++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsEvent.kt
@@ -4,9 +4,4 @@ sealed interface DetailsEvent {
data class OnOpenRepositoryInApp(val repositoryId: Long) : DetailsEvent
data class InstallTrackingFailed(val message: String) : DetailsEvent
data class OnMessage(val message: String) : DetailsEvent
- data class ShowDowngradeWarning(
- val packageName: String,
- val currentVersion: String,
- val targetVersion: String
- ) : DetailsEvent
}
\ No newline at end of file
diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
index d2ef6fb1..1ad4155d 100644
--- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
+++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt
@@ -20,7 +20,6 @@ import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.StarBorder
-import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -32,16 +31,13 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
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.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
@@ -81,9 +77,6 @@ fun DetailsRoot(
val state by viewModel.state.collectAsStateWithLifecycle()
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
- var downgradeWarning by remember {
- mutableStateOf(null)
- }
ObserveAsEvents(viewModel.events) { event ->
when (event) {
@@ -100,49 +93,9 @@ fun DetailsRoot(
snackbarHostState.showSnackbar(event.message)
}
}
-
- is DetailsEvent.ShowDowngradeWarning -> {
- downgradeWarning = event
- }
}
}
- downgradeWarning?.let { warning ->
- AlertDialog(
- onDismissRequest = { downgradeWarning = null },
- title = {
- Text(text = stringResource(Res.string.downgrade_requires_uninstall))
- },
- text = {
- Text(
- text = stringResource(
- Res.string.downgrade_warning_message,
- warning.targetVersion,
- warning.currentVersion
- )
- )
- },
- confirmButton = {
- TextButton(
- onClick = {
- downgradeWarning = null
- viewModel.onAction(DetailsAction.UninstallApp)
- }
- ) {
- Text(
- text = stringResource(Res.string.uninstall_first),
- color = MaterialTheme.colorScheme.error
- )
- }
- },
- dismissButton = {
- TextButton(onClick = { downgradeWarning = null }) {
- Text(text = stringResource(Res.string.cancel))
- }
- }
- )
- }
-
DetailsScreen(
state = state,
snackbarHostState = snackbarHostState,
diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
index 7028d87b..7aafb74e 100644
--- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
+++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsViewModel.kt
@@ -330,10 +330,12 @@ class DetailsViewModel(
if (isDowngrade) {
viewModelScope.launch {
_events.send(
- DetailsEvent.ShowDowngradeWarning(
- packageName = installedApp.packageName,
- currentVersion = installedApp.installedVersion,
- targetVersion = release.tagName
+ DetailsEvent.OnMessage(
+ getString(
+ Res.string.downgrade_warning_message,
+ release.tagName,
+ installedApp.installedVersion
+ )
)
)
}
@@ -470,24 +472,6 @@ class DetailsViewModel(
}
}
- DetailsAction.UninstallApp -> {
- val installedApp = _state.value.installedApp ?: return
- logger.debug("Uninstalling app: ${installedApp.packageName}")
- viewModelScope.launch {
- try {
- installer.uninstall(installedApp.packageName)
- } catch (e: Exception) {
- logger.error("Failed to request uninstall for ${installedApp.packageName}: ${e.message}")
- _events.send(
- DetailsEvent.OnMessage(
- getString(Res.string.failed_to_uninstall, installedApp.packageName)
- )
- )
- }
- }
-
- }
-
DetailsAction.OpenApp -> {
val installedApp = _state.value.installedApp ?: return
val launched = installer.openApp(installedApp.packageName)
diff --git a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt
index d24603da..53be6573 100644
--- a/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt
+++ b/feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/components/SmartInstallButton.kt
@@ -17,7 +17,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.CardDefaults
@@ -77,92 +76,38 @@ fun SmartInstallButton(
val isActiveDownload = state.isDownloading || state.downloadStage != DownloadStage.IDLE
- // When same version is installed, show Open + Uninstall (Play Store style)
+ // When same version is installed, show Open button
if (isSameVersionInstalled && !isActiveDownload) {
- Row(
- modifier = modifier,
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(4.dp)
+ ElevatedCard(
+ modifier = modifier
+ .height(52.dp)
+ .clickable { onAction(DetailsAction.OpenApp) }
+ .liquefiable(liquidState),
+ colors = CardDefaults.elevatedCardColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ ),
+ shape = CircleShape
) {
- // Uninstall button
- ElevatedCard(
- onClick = { onAction(DetailsAction.UninstallApp) },
- modifier = Modifier
- .weight(1f)
- .height(52.dp)
- .liquefiable(liquidState),
- colors = CardDefaults.elevatedCardColors(
- containerColor = MaterialTheme.colorScheme.errorContainer
- ),
- shape = RoundedCornerShape(
- topStart = 24.dp,
- bottomStart = 24.dp,
- topEnd = 6.dp,
- bottomEnd = 6.dp
- )
- ) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(6.dp)
- ) {
- Icon(
- imageVector = Icons.Default.Delete,
- contentDescription = null,
- modifier = Modifier.size(18.dp),
- tint = MaterialTheme.colorScheme.onErrorContainer
- )
- Text(
- text = stringResource(Res.string.uninstall),
- color = MaterialTheme.colorScheme.onErrorContainer,
- fontWeight = FontWeight.Bold,
- style = MaterialTheme.typography.titleMedium
- )
- }
- }
- }
-
- // Open button
- ElevatedCard(
- modifier = Modifier
- .weight(1f)
- .height(52.dp)
- .clickable { onAction(DetailsAction.OpenApp) }
- .liquefiable(liquidState),
- colors = CardDefaults.elevatedCardColors(
- containerColor = MaterialTheme.colorScheme.primary
- ),
- shape = RoundedCornerShape(
- topStart = 6.dp,
- bottomStart = 6.dp,
- topEnd = 24.dp,
- bottomEnd = 24.dp
- )
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
) {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(6.dp)
- ) {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.OpenInNew,
- contentDescription = null,
- modifier = Modifier.size(18.dp),
- tint = MaterialTheme.colorScheme.onPrimary
- )
- Text(
- text = stringResource(Res.string.open_app),
- color = MaterialTheme.colorScheme.onPrimary,
- fontWeight = FontWeight.Bold,
- style = MaterialTheme.typography.titleMedium
- )
- }
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.OpenInNew,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp),
+ tint = MaterialTheme.colorScheme.onPrimary
+ )
+ Text(
+ text = stringResource(Res.string.open_app),
+ color = MaterialTheme.colorScheme.onPrimary,
+ fontWeight = FontWeight.Bold,
+ style = MaterialTheme.typography.titleMedium
+ )
}
}
}
diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt
index 746f2d71..6aae32ee 100644
--- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt
+++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileAction.kt
@@ -13,4 +13,11 @@ sealed interface ProfileAction {
data object OnLogoutDismiss : ProfileAction
data object OnHelpClick : ProfileAction
data class OnFontThemeSelected(val fontTheme: FontTheme) : ProfileAction
+ data class OnProxyTypeSelected(val type: ProxyType) : ProfileAction
+ data class OnProxyHostChanged(val host: String) : ProfileAction
+ data class OnProxyPortChanged(val port: String) : ProfileAction
+ data class OnProxyUsernameChanged(val username: String) : ProfileAction
+ data class OnProxyPasswordChanged(val password: String) : ProfileAction
+ data object OnProxyPasswordVisibilityToggle : ProfileAction
+ data object OnProxySave : ProfileAction
}
\ No newline at end of file
diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt
index e77a358c..9bb5f0ae 100644
--- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt
+++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileEvent.kt
@@ -3,4 +3,6 @@ package zed.rainxch.profile.presentation
sealed interface ProfileEvent {
data object OnLogoutSuccessful : ProfileEvent
data class OnLogoutError(val message: String) : ProfileEvent
+ data object OnProxySaved : ProfileEvent
+ data class OnProxySaveError(val message: String) : ProfileEvent
}
\ No newline at end of file
diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt
index e1e719ea..a378a8f5 100644
--- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt
+++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt
@@ -2,6 +2,7 @@ package zed.rainxch.profile.presentation
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -34,12 +35,14 @@ import org.jetbrains.compose.resources.getString
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.koin.compose.viewmodel.koinViewModel
+import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
import zed.rainxch.core.presentation.theme.GithubStoreTheme
import zed.rainxch.core.presentation.utils.ObserveAsEvents
import zed.rainxch.profile.presentation.components.LogoutDialog
import zed.rainxch.profile.presentation.components.sections.about
import zed.rainxch.profile.presentation.components.sections.logout
+import zed.rainxch.profile.presentation.components.sections.networkSection
import zed.rainxch.profile.presentation.components.sections.profile
import zed.rainxch.profile.presentation.components.sections.settings
@@ -67,6 +70,18 @@ fun ProfileRoot(
snackbarState.showSnackbar(event.message)
}
}
+
+ ProfileEvent.OnProxySaved -> {
+ coroutineScope.launch {
+ snackbarState.showSnackbar(getString(Res.string.proxy_saved))
+ }
+ }
+
+ is ProfileEvent.OnProxySaveError -> {
+ coroutineScope.launch {
+ snackbarState.showSnackbar(event.message)
+ }
+ }
}
}
@@ -106,9 +121,13 @@ fun ProfileScreen(
snackbarState: SnackbarHostState
) {
val liquidState = LocalBottomNavigationLiquid.current
+ val bottomNavHeight = LocalBottomNavigationHeight.current
Scaffold(
snackbarHost = {
- SnackbarHost(hostState = snackbarState)
+ SnackbarHost(
+ hostState = snackbarState,
+ modifier = Modifier.padding(bottom = bottomNavHeight)
+ )
},
topBar = {
TopAppBar(onAction)
@@ -140,6 +159,15 @@ fun ProfileScreen(
Spacer(Modifier.height(16.dp))
}
+ networkSection(
+ state = state,
+ onAction = onAction
+ )
+
+ item {
+ Spacer(Modifier.height(16.dp))
+ }
+
about(
versionName = state.versionName,
onAction = onAction
diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt
index fd676000..b474cc58 100644
--- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt
+++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileState.kt
@@ -2,6 +2,7 @@ package zed.rainxch.profile.presentation
import zed.rainxch.core.domain.model.AppTheme
import zed.rainxch.core.domain.model.FontTheme
+import zed.rainxch.core.domain.model.ProxyConfig
import zed.rainxch.profile.domain.model.UserProfile
data class ProfileState(
@@ -12,5 +13,24 @@ data class ProfileState(
val isUserLoggedIn: Boolean = false,
val isAmoledThemeEnabled: Boolean = false,
val isDarkTheme: Boolean? = null,
- val versionName: String = ""
-)
\ No newline at end of file
+ val versionName: String = "",
+ val proxyType: ProxyType = ProxyType.NONE,
+ val proxyHost: String = "",
+ val proxyPort: String = "",
+ val proxyUsername: String = "",
+ val proxyPassword: String = "",
+ val isProxyPasswordVisible: Boolean = false,
+)
+
+enum class ProxyType {
+ NONE, SYSTEM, HTTP, SOCKS;
+
+ companion object {
+ fun fromConfig(config: ProxyConfig): ProxyType = when (config) {
+ is ProxyConfig.None -> NONE
+ is ProxyConfig.System -> SYSTEM
+ is ProxyConfig.Http -> HTTP
+ is ProxyConfig.Socks -> SOCKS
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt
index c08726bf..a0b06064 100644
--- a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt
+++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt
@@ -10,14 +10,23 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.jetbrains.compose.resources.getString
+import org.jetbrains.compose.resources.stringResource
+import zed.rainxch.core.domain.model.ProxyConfig
+import zed.rainxch.core.domain.repository.ProxyRepository
import zed.rainxch.core.domain.repository.ThemesRepository
import zed.rainxch.core.domain.utils.BrowserHelper
+import zed.rainxch.githubstore.core.presentation.res.Res
+import zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settings
+import zed.rainxch.githubstore.core.presentation.res.invalid_proxy_port
+import zed.rainxch.githubstore.core.presentation.res.proxy_host_required
import zed.rainxch.profile.domain.repository.ProfileRepository
class ProfileViewModel(
private val browserHelper: BrowserHelper,
private val themesRepository: ThemesRepository,
- private val profileRepository: ProfileRepository
+ private val profileRepository: ProfileRepository,
+ private val proxyRepository: ProxyRepository
) : ViewModel() {
private var hasLoadedInitialData = false
@@ -29,6 +38,7 @@ class ProfileViewModel(
loadCurrentTheme()
collectIsUserLoggedIn()
loadVersionName()
+ loadProxyConfig()
hasLoadedInitialData = true
}
@@ -95,6 +105,38 @@ class ProfileViewModel(
}
}
+ private fun loadProxyConfig() {
+ viewModelScope.launch {
+ proxyRepository.getProxyConfig().collect { config ->
+ _state.update {
+ it.copy(
+ proxyType = ProxyType.fromConfig(config),
+ proxyHost = when (config) {
+ is ProxyConfig.Http -> config.host
+ is ProxyConfig.Socks -> config.host
+ else -> it.proxyHost
+ },
+ proxyPort = when (config) {
+ is ProxyConfig.Http -> config.port.toString()
+ is ProxyConfig.Socks -> config.port.toString()
+ else -> it.proxyPort
+ },
+ proxyUsername = when (config) {
+ is ProxyConfig.Http -> config.username ?: ""
+ is ProxyConfig.Socks -> config.username ?: ""
+ else -> it.proxyUsername
+ },
+ proxyPassword = when (config) {
+ is ProxyConfig.Http -> config.password ?: ""
+ is ProxyConfig.Socks -> config.password ?: ""
+ else -> it.proxyPassword
+ }
+ )
+ }
+ }
+ }
+ }
+
fun onAction(action: ProfileAction) {
when (action) {
ProfileAction.OnHelpClick -> {
@@ -162,7 +204,94 @@ class ProfileViewModel(
themesRepository.setDarkTheme(action.isDarkTheme)
}
}
+
+ is ProfileAction.OnProxyTypeSelected -> {
+ _state.update { it.copy(proxyType = action.type) }
+ if (action.type == ProxyType.NONE || action.type == ProxyType.SYSTEM) {
+ val config = when (action.type) {
+ ProxyType.NONE -> ProxyConfig.None
+ ProxyType.SYSTEM -> ProxyConfig.System
+ else -> return
+ }
+ viewModelScope.launch {
+ runCatching {
+ proxyRepository.setProxyConfig(config)
+ }.onSuccess {
+ _events.send(ProfileEvent.OnProxySaved)
+ }.onFailure { error ->
+ _events.send(
+ ProfileEvent.OnProxySaveError(
+ error.message ?: getString(Res.string.failed_to_save_proxy_settings)
+ )
+ )
+ }
+
+ }
+ }
+ }
+
+ is ProfileAction.OnProxyHostChanged -> {
+ _state.update { it.copy(proxyHost = action.host) }
+ }
+
+ is ProfileAction.OnProxyPortChanged -> {
+ _state.update { it.copy(proxyPort = action.port) }
+ }
+
+ is ProfileAction.OnProxyUsernameChanged -> {
+ _state.update { it.copy(proxyUsername = action.username) }
+ }
+
+ is ProfileAction.OnProxyPasswordChanged -> {
+ _state.update { it.copy(proxyPassword = action.password) }
+ }
+
+ ProfileAction.OnProxyPasswordVisibilityToggle -> {
+ _state.update { it.copy(isProxyPasswordVisible = !it.isProxyPasswordVisible) }
+ }
+
+ ProfileAction.OnProxySave -> {
+ val currentState = _state.value
+ val port = currentState.proxyPort.toIntOrNull()
+ ?.takeIf { it in 1..65535 }
+ ?: run {
+ viewModelScope.launch {
+ _events.send(ProfileEvent.OnProxySaveError(getString(Res.string.invalid_proxy_port)))
+ }
+ return
+ }
+ val host = currentState.proxyHost.trim().takeIf { it.isNotBlank() } ?: run {
+ viewModelScope.launch {
+ _events.send(ProfileEvent.OnProxySaveError(getString(Res.string.proxy_host_required)))
+ }
+ return
+ }
+
+ val username = currentState.proxyUsername.takeIf { it.isNotBlank() }
+ val password = currentState.proxyPassword.takeIf { it.isNotBlank() }
+
+ val config = when (currentState.proxyType) {
+ ProxyType.HTTP -> ProxyConfig.Http(host, port, username, password)
+ ProxyType.SOCKS -> ProxyConfig.Socks(host, port, username, password)
+ ProxyType.NONE -> ProxyConfig.None
+ ProxyType.SYSTEM -> ProxyConfig.System
+ }
+
+ viewModelScope.launch {
+ runCatching {
+ proxyRepository.setProxyConfig(config)
+ }.onSuccess {
+ _events.send(ProfileEvent.OnProxySaved)
+ }.onFailure { error ->
+ _events.send(
+ ProfileEvent.OnProxySaveError(
+ error.message ?: getString(Res.string.failed_to_save_proxy_settings)
+ )
+ )
+ }
+ }
+ }
}
}
-}
\ No newline at end of file
+}
diff --git a/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt
new file mode 100644
index 00000000..f5a06b39
--- /dev/null
+++ b/feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt
@@ -0,0 +1,276 @@
+package zed.rainxch.profile.presentation.components.sections
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyListScope
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Save
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.FilledTonalButton
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.resources.stringResource
+import zed.rainxch.githubstore.core.presentation.res.*
+import zed.rainxch.profile.presentation.ProfileAction
+import zed.rainxch.profile.presentation.ProfileState
+import zed.rainxch.profile.presentation.ProxyType
+import zed.rainxch.profile.presentation.components.SectionHeader
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+fun LazyListScope.networkSection(
+ state: ProfileState,
+ onAction: (ProfileAction) -> Unit,
+) {
+ item {
+ SectionHeader(
+ text = stringResource(Res.string.section_network)
+ )
+
+ Spacer(Modifier.height(8.dp))
+
+ ProxyTypeCard(
+ selectedType = state.proxyType,
+ onTypeSelected = { type ->
+ onAction(ProfileAction.OnProxyTypeSelected(type))
+ }
+ )
+
+ // Description text for None / System proxy types
+ AnimatedVisibility(
+ visible = state.proxyType == ProxyType.NONE || state.proxyType == ProxyType.SYSTEM,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Text(
+ text = when (state.proxyType) {
+ ProxyType.SYSTEM -> stringResource(Res.string.proxy_system_description)
+ else -> stringResource(Res.string.proxy_none_description)
+ },
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(start = 8.dp, top = 12.dp)
+ )
+ }
+
+ // Details card for HTTP / SOCKS proxy types
+ AnimatedVisibility(
+ visible = state.proxyType == ProxyType.HTTP || state.proxyType == ProxyType.SOCKS,
+ enter = expandVertically() + fadeIn(),
+ exit = shrinkVertically() + fadeOut()
+ ) {
+ Column {
+ Spacer(Modifier.height(16.dp))
+
+ ProxyDetailsCard(
+ state = state,
+ onAction = onAction
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun ProxyTypeCard(
+ selectedType: ProxyType,
+ onTypeSelected: (ProxyType) -> Unit
+) {
+ ElevatedCard(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.elevatedCardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer
+ ),
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Text(
+ text = stringResource(Res.string.proxy_type),
+ style = MaterialTheme.typography.titleMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontWeight = FontWeight.SemiBold
+ )
+
+ Spacer(Modifier.height(12.dp))
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ ProxyType.entries.forEach { type ->
+ FilterChip(
+ selected = selectedType == type,
+ onClick = { onTypeSelected(type) },
+ label = {
+ Text(
+ text = when (type) {
+ ProxyType.NONE -> stringResource(Res.string.proxy_none)
+ ProxyType.SYSTEM -> stringResource(Res.string.proxy_system)
+ ProxyType.HTTP -> stringResource(Res.string.proxy_http)
+ ProxyType.SOCKS -> stringResource(Res.string.proxy_socks)
+ },
+ fontWeight = if (selectedType == type) FontWeight.Bold else FontWeight.Normal,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurface,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+private fun ProxyDetailsCard(
+ state: ProfileState,
+ onAction: (ProfileAction) -> Unit
+) {
+ val portValue = state.proxyPort
+ val isPortInvalid = portValue.isNotEmpty() &&
+ (portValue.toIntOrNull()?.let { it !in 1..65535 } ?: true)
+ val isFormValid = state.proxyHost.isNotBlank() &&
+ portValue.isNotEmpty() &&
+ portValue.toIntOrNull()?.let { it in 1..65535 } == true
+
+ ElevatedCard(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.elevatedCardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainer
+ ),
+ shape = RoundedCornerShape(20.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ // Host + Port row
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ OutlinedTextField(
+ value = state.proxyHost,
+ onValueChange = { onAction(ProfileAction.OnProxyHostChanged(it)) },
+ label = { Text(stringResource(Res.string.proxy_host)) },
+ placeholder = { Text("127.0.0.1") },
+ singleLine = true,
+ modifier = Modifier.weight(2f),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ OutlinedTextField(
+ value = state.proxyPort,
+ onValueChange = { onAction(ProfileAction.OnProxyPortChanged(it)) },
+ label = { Text(stringResource(Res.string.proxy_port)) },
+ placeholder = { Text("1080") },
+ singleLine = true,
+ isError = isPortInvalid,
+ supportingText = if (isPortInvalid) {
+ { Text(stringResource(Res.string.proxy_port_error)) }
+ } else null,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
+ modifier = Modifier.weight(1f),
+ shape = RoundedCornerShape(12.dp)
+ )
+ }
+
+ // Username
+ OutlinedTextField(
+ value = state.proxyUsername,
+ onValueChange = { onAction(ProfileAction.OnProxyUsernameChanged(it)) },
+ label = { Text(stringResource(Res.string.proxy_username)) },
+ singleLine = true,
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ // Password with visibility toggle
+ OutlinedTextField(
+ value = state.proxyPassword,
+ onValueChange = { onAction(ProfileAction.OnProxyPasswordChanged(it)) },
+ label = { Text(stringResource(Res.string.proxy_password)) },
+ singleLine = true,
+ visualTransformation = if (state.isProxyPasswordVisible) {
+ VisualTransformation.None
+ } else {
+ PasswordVisualTransformation()
+ },
+ trailingIcon = {
+ IconButton(
+ onClick = { onAction(ProfileAction.OnProxyPasswordVisibilityToggle) }
+ ) {
+ Icon(
+ imageVector = if (state.isProxyPasswordVisible) {
+ Icons.Default.VisibilityOff
+ } else {
+ Icons.Default.Visibility
+ },
+ contentDescription = if (state.isProxyPasswordVisible) {
+ stringResource(Res.string.proxy_hide_password)
+ } else {
+ stringResource(Res.string.proxy_show_password)
+ },
+
+ modifier = Modifier.size(20.dp)
+ )
+ }
+ },
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp)
+ )
+
+ // Save button
+ FilledTonalButton(
+ onClick = { onAction(ProfileAction.OnProxySave) },
+ enabled = isFormValid,
+ modifier = Modifier.align(Alignment.End)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Save,
+ contentDescription = null,
+ modifier = Modifier.size(18.dp)
+ )
+ Spacer(Modifier.size(8.dp))
+ Text(stringResource(Res.string.proxy_save))
+ }
+ }
+ }
+}
diff --git a/feature/settings/CLAUDE.md b/feature/settings/CLAUDE.md
deleted file mode 100644
index 991b9190..00000000
--- a/feature/settings/CLAUDE.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# CLAUDE.md - Settings Feature
-
-## Purpose
-
-App settings and preferences. Organized into three sections: **Account** (login status, logout), **Appearance** (theme colors, dark mode, AMOLED, font), and **About** (version info, links).
-
-## Module Structure
-
-```
-feature/settings/
-├── domain/
-│ └── repository/SettingsRepository.kt # Login state, logout, version
-├── data/
-│ ├── di/SharedModule.kt # Koin: settingsModule
-│ └── repository/SettingsRepositoryImpl.kt
-└── presentation/
- ├── SettingsViewModel.kt # Settings state, theme changes, logout
- ├── SettingsState.kt # isLoggedIn, theme, darkMode, amoled, font, version
- ├── SettingsAction.kt # Theme change, toggle dark/amoled, logout, etc.
- ├── SettingsEvent.kt # One-off events
- ├── SettingsRoot.kt # Main composable (scrollable settings list)
- └── components/
- ├── LogoutDialog.kt # Confirmation dialog for logout
- └── sections/
- ├── Account.kt # Login status + logout button
- ├── Appearance.kt # Theme picker, dark mode, AMOLED, font
- └── About.kt # Version, links, credits
-```
-
-## Key Interfaces
-
-```kotlin
-interface SettingsRepository {
- val isUserLoggedIn: Flow
- suspend fun logout()
- fun getVersionName(): String
-}
-```
-
-## Navigation
-
-Route: `GithubStoreGraph.SettingsScreen` (data object, no params)
-
-## Implementation Notes
-
-- Theme settings persist via `ThemesRepository` from core/domain (DataStore-backed)
-- `SettingsViewModel` also depends on `ThemesRepository` for reading/writing appearance preferences
-- Logout clears the token via `TokenStore` and resets auth state
-- App theme (colors, dark mode, AMOLED, font) is applied app-wide through `MainViewModel` which observes `ThemesRepository`
-- Version name is read from build config at runtime