Просмотр исходного кода

Add initial project structure with license, privacy policy, and README. Implement core functionality including data models, repository, and use cases for wallet management and network information. Set up UI components for dashboard, settings, and wallet screens. Enhance theming and localization support.

Carles Sentis 3 дней назад
Родитель
Сommit
29b05c5fff
90 измененных файлов с 3663 добавлено и 297 удалено
  1. 1 0
      .idea/gradle.xml
  2. 1 1
      .idea/kotlinc.xml
  3. 6 0
      .idea/vcs.xml
  4. 58 0
      .kotlin/errors/errors-1743755687652.log
  5. 21 0
      LICENSE
  6. 27 0
      README.md
  7. 35 10
      app/build.gradle.kts
  8. 4 2
      app/src/main/AndroidManifest.xml
  9. BIN
      app/src/main/ic_launcher-playstore.png
  10. 65 20
      app/src/main/java/com/codeskraps/publicpool/MainActivity.kt
  11. 29 0
      app/src/main/java/com/codeskraps/publicpool/MainApplication.kt
  12. 7 0
      app/src/main/java/com/codeskraps/publicpool/data/local/PreferencesKeys.kt
  13. 118 0
      app/src/main/java/com/codeskraps/publicpool/data/mappers/Mappers.kt
  14. 65 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/KtorApiService.kt
  15. 11 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/dto/BinanceTickerDto.kt
  16. 79 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/dto/BlockchainInfoDtos.kt
  17. 13 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/dto/ChartDataPointDto.kt
  18. 10 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/dto/ClientInfoDto.kt
  19. 16 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/dto/NetworkInfoDto.kt
  20. 13 0
      app/src/main/java/com/codeskraps/publicpool/data/remote/dto/WorkerDto.kt
  21. 124 0
      app/src/main/java/com/codeskraps/publicpool/data/repository/PublicPoolRepositoryImpl.kt
  22. 22 0
      app/src/main/java/com/codeskraps/publicpool/di/AppModule.kt
  23. 14 0
      app/src/main/java/com/codeskraps/publicpool/di/AppReadinessState.kt
  24. 44 0
      app/src/main/java/com/codeskraps/publicpool/di/DataModule.kt
  25. 16 0
      app/src/main/java/com/codeskraps/publicpool/di/DomainModule.kt
  26. 15 0
      app/src/main/java/com/codeskraps/publicpool/di/PresentationModule.kt
  27. 44 0
      app/src/main/java/com/codeskraps/publicpool/domain/model/BlockchainInfoModels.kt
  28. 8 0
      app/src/main/java/com/codeskraps/publicpool/domain/model/ChartDataPoint.kt
  29. 7 0
      app/src/main/java/com/codeskraps/publicpool/domain/model/ClientInfo.kt
  30. 10 0
      app/src/main/java/com/codeskraps/publicpool/domain/model/CryptoPrice.kt
  31. 8 0
      app/src/main/java/com/codeskraps/publicpool/domain/model/NetworkInfo.kt
  32. 10 0
      app/src/main/java/com/codeskraps/publicpool/domain/model/Worker.kt
  33. 28 0
      app/src/main/java/com/codeskraps/publicpool/domain/repository/PublicPoolRepository.kt
  34. 44 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/CalculateTwoHourAverageUseCase.kt
  35. 15 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetBlockchainWalletInfoUseCase.kt
  36. 13 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetBtcPriceUseCase.kt
  37. 13 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetChartDataUseCase.kt
  38. 13 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetClientInfoUseCase.kt
  39. 10 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetNetworkInfoUseCase.kt
  40. 10 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetWalletAddressUseCase.kt
  41. 12 0
      app/src/main/java/com/codeskraps/publicpool/domain/usecase/SaveWalletAddressUseCase.kt
  42. 33 0
      app/src/main/java/com/codeskraps/publicpool/presentation/common/AppCard.kt
  43. 18 0
      app/src/main/java/com/codeskraps/publicpool/presentation/common/MviBase.kt
  44. 374 0
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardContent.kt
  45. 51 0
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardMvi.kt
  46. 177 0
      app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardScreenModel.kt
  47. 62 0
      app/src/main/java/com/codeskraps/publicpool/presentation/navigation/BottomTabs.kt
  48. 47 0
      app/src/main/java/com/codeskraps/publicpool/presentation/navigation/Screens.kt
  49. 88 0
      app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsContent.kt
  50. 24 0
      app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsMvi.kt
  51. 67 0
      app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsScreenModel.kt
  52. 33 0
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletMvi.kt
  53. 321 0
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreen.kt
  54. 105 0
      app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreenModel.kt
  55. 28 0
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersMvi.kt
  56. 270 0
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreen.kt
  57. 91 0
      app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreenModel.kt
  58. 12 6
      app/src/main/java/com/codeskraps/publicpool/ui/theme/Color.kt
  59. 44 33
      app/src/main/java/com/codeskraps/publicpool/ui/theme/Theme.kt
  60. 173 0
      app/src/main/java/com/codeskraps/publicpool/util/Formatters.kt
  61. 5 0
      app/src/main/res/drawable/device_hub.xml
  62. 5 0
      app/src/main/res/drawable/expand_less.xml
  63. 5 0
      app/src/main/res/drawable/expand_more.xml
  64. 0 170
      app/src/main/res/drawable/ic_launcher_background.xml
  65. 3 27
      app/src/main/res/drawable/ic_launcher_foreground.xml
  66. 5 0
      app/src/main/res/drawable/wallet.xml
  67. 2 3
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  68. 2 3
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  69. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  70. BIN
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  71. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  72. BIN
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  73. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  74. BIN
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  75. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  76. BIN
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  77. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  78. BIN
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  79. 61 0
      app/src/main/res/values-de/strings.xml
  80. 61 0
      app/src/main/res/values-es/strings.xml
  81. 61 0
      app/src/main/res/values-fr/strings.xml
  82. 61 0
      app/src/main/res/values-hi/strings.xml
  83. 61 0
      app/src/main/res/values-zh-rCN/strings.xml
  84. 1 7
      app/src/main/res/values/colors.xml
  85. 4 0
      app/src/main/res/values/ic_launcher_background.xml
  86. 59 0
      app/src/main/res/values/strings.xml
  87. 14 0
      app/src/main/res/values/themes.xml
  88. 19 15
      gradle/libs.versions.toml
  89. 155 0
      privacy_policy.md
  90. 2 0
      settings.gradle.kts

+ 1 - 0
.idea/gradle.xml

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
   <component name="GradleSettings">
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>

+ 1 - 1
.idea/kotlinc.xml

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="KotlinJpsPluginSettings">
-    <option name="version" value="2.0.21" />
+    <option name="version" value="2.1.20" />
   </component>
 </project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 58 - 0
.kotlin/errors/errors-1743755687652.log

@@ -0,0 +1,58 @@
+kotlin version: 2.0.21
+error message: java.lang.IncompatibleClassChangeError: class com.google.devtools.ksp.common.PersistentMap cannot inherit from final class org.jetbrains.kotlin.com.intellij.util.io.PersistentHashMap
+	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
+	at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
+	at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
+	at java.base/java.net.URLClassLoader.defineClass(Unknown Source)
+	at java.base/java.net.URLClassLoader$1.run(Unknown Source)
+	at java.base/java.net.URLClassLoader$1.run(Unknown Source)
+	at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+	at java.base/java.net.URLClassLoader.findClass(Unknown Source)
+	at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
+	at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
+	at java.base/java.lang.ClassLoader.defineClass1(Native Method)
+	at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
+	at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
+	at java.base/java.net.URLClassLoader.defineClass(Unknown Source)
+	at java.base/java.net.URLClassLoader$1.run(Unknown Source)
+	at java.base/java.net.URLClassLoader$1.run(Unknown Source)
+	at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+	at java.base/java.net.URLClassLoader.findClass(Unknown Source)
+	at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
+	at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
+	at com.google.devtools.ksp.common.IncrementalContextBase.<init>(IncrementalContextBase.kt:103)
+	at com.google.devtools.ksp.IncrementalContext.<init>(IncrementalContext.kt:64)
+	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:192)
+	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension$doAnalysis$2.invoke(KotlinSymbolProcessingExtension.kt:189)
+	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.handleException(KotlinSymbolProcessingExtension.kt:414)
+	at com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension.doAnalysis(KotlinSymbolProcessingExtension.kt:189)
+	at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(TopDownAnalyzerFacadeForJVM.kt:112)
+	at org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration$default(TopDownAnalyzerFacadeForJVM.kt:75)
+	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze$lambda$12(KotlinToJVMBytecodeCompiler.kt:373)
+	at org.jetbrains.kotlin.cli.common.messages.AnalyzerWithCompilerReport.analyzeAndReport(AnalyzerWithCompilerReport.kt:112)
+	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.analyze(KotlinToJVMBytecodeCompiler.kt:364)
+	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.runFrontendAndGenerateIrUsingClassicFrontend(KotlinToJVMBytecodeCompiler.kt:195)
+	at org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler.compileModules$cli(KotlinToJVMBytecodeCompiler.kt:106)
+	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:170)
+	at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
+	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
+	at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
+	at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
+	at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1555)
+	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
+	at java.base/java.lang.reflect.Method.invoke(Unknown Source)
+	at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
+	at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+	at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
+	at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+	at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
+	at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
+	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
+	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
+	at java.base/java.security.AccessController.doPrivileged(Unknown Source)
+	at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
+	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
+	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
+	at java.base/java.lang.Thread.run(Unknown Source)
+
+

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 [codeskraps]
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. 

+ 27 - 0
README.md

@@ -0,0 +1,27 @@
+# Project Title
+
+A brief description of what this project does and who it's for.
+
+## Installation
+
+Instructions on how to install the project or dependencies.
+
+```bash
+# Example installation command
+```
+
+## Usage
+
+How to use the project. Provide examples or code snippets.
+
+```python
+# Example usage
+```
+
+## Contributing
+
+Information on how to contribute to the project.
+
+## License
+
+Specify the project license. 

+ 35 - 10
app/build.gradle.kts

@@ -4,6 +4,7 @@ plugins {
     alias(libs.plugins.kotlin.compose)
     alias(libs.plugins.ksp)
     alias(libs.plugins.kotlin.serialization)
+    id("kotlin-parcelize")
 }
 
 android {
@@ -12,7 +13,7 @@ android {
 
     defaultConfig {
         applicationId = "com.codeskraps.publicpool"
-        minSdk = 24
+        minSdk = 26
         targetSdk = 35
         versionCode = 1
         versionName = "1.0"
@@ -33,11 +34,11 @@ android {
         }
     }
     compileOptions {
-        sourceCompatibility = JavaVersion.VERSION_11
-        targetCompatibility = JavaVersion.VERSION_11
+        sourceCompatibility = JavaVersion.VERSION_19
+        targetCompatibility = JavaVersion.VERSION_19
     }
     kotlinOptions {
-        jvmTarget = "11"
+        jvmTarget = JavaVersion.VERSION_19.toString()
     }
     buildFeatures {
         compose = true
@@ -49,42 +50,66 @@ android {
         resources {
             excludes += "/META-INF/{AL2.0,LGPL2.1}"
             excludes += "/META-INF/services/javax.annotation.processing.Processor"
+            excludes += "/META-INF/INDEX.LIST"
         }
     }
 }
 
 dependencies {
 
+    // AndroidX Core & Lifecycle
     implementation(libs.androidx.core.ktx)
+    implementation(libs.androidx.core.splashscreen)
     implementation(libs.androidx.lifecycle.runtime.ktx)
     implementation(libs.androidx.activity.compose)
+
+    // Compose
     implementation(platform(libs.androidx.compose.bom))
     implementation(libs.androidx.ui)
     implementation(libs.androidx.ui.graphics)
     implementation(libs.androidx.ui.tooling.preview)
     implementation(libs.androidx.material3)
-    implementation(libs.androidx.navigation.compose)
+    implementation(libs.androidx.navigation.compose) // Consider if this belongs with Voyager or Compose UI
+
+    // Networking (Ktor)
     implementation(libs.ktor.client.core)
     implementation(libs.ktor.client.android)
     implementation(libs.ktor.client.content.negotiation)
     implementation(libs.ktor.serialization.kotlinx.json)
     implementation(libs.ktor.client.logging)
-    implementation(libs.logback.classic)
-    implementation(libs.androidx.room.runtime)
-    implementation(libs.androidx.room.ktx)
-    ksp(libs.androidx.room.compiler)
+    implementation(libs.logback.classic) // Logging backend for Ktor
+
+    // Data Persistence
+    implementation(libs.androidx.datastore.preferences)
+
+    // Dependency Injection (Koin)
     implementation(platform(libs.koin.bom))
     implementation(libs.koin.android)
     implementation(libs.koin.androidx.compose)
+
+    // Charting
     implementation(libs.anychart.android.core)
-    implementation(libs.androidx.datastore.preferences)
+
+    // Image Loading
     implementation(libs.coil.compose)
+
+    // Serialization
     implementation(libs.kotlinx.serialization.json)
+
+    // Navigation (Voyager)
+    implementation(libs.voyager.navigator)
+    implementation(libs.voyager.koin)
+    implementation(libs.voyager.transitions)
+    implementation(libs.voyager.tab.navigator)
+
+    // Testing
     testImplementation(libs.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.espresso.core)
     androidTestImplementation(platform(libs.androidx.compose.bom))
     androidTestImplementation(libs.androidx.ui.test.junit4)
+
+    // Debugging
     debugImplementation(libs.androidx.ui.tooling)
     debugImplementation(libs.androidx.ui.test.manifest)
 }

+ 4 - 2
app/src/main/AndroidManifest.xml

@@ -2,7 +2,10 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools">
 
+    <uses-permission android:name="android.permission.INTERNET" />
+
     <application
+        android:name=".MainApplication"
         android:allowBackup="true"
         android:dataExtractionRules="@xml/data_extraction_rules"
         android:fullBackupContent="@xml/backup_rules"
@@ -15,8 +18,7 @@
         <activity
             android:name=".MainActivity"
             android:exported="true"
-            android:label="@string/app_name"
-            android:theme="@style/Theme.PublicPool">
+            android:theme="@style/Theme.App.Starting">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 

BIN
app/src/main/ic_launcher-playstore.png


+ 65 - 20
app/src/main/java/com/codeskraps/publicpool/MainActivity.kt

@@ -1,47 +1,92 @@
 package com.codeskraps.publicpool
 
+import android.annotation.SuppressLint
 import android.os.Bundle
+import android.view.View
+import android.view.ViewTreeObserver
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
 import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Icon
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import cafe.adriel.voyager.navigator.CurrentScreen
+import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
+import cafe.adriel.voyager.navigator.tab.Tab
+import cafe.adriel.voyager.navigator.tab.TabNavigator
+import com.codeskraps.publicpool.di.AppReadinessState
+import com.codeskraps.publicpool.presentation.navigation.DashboardTab
+import com.codeskraps.publicpool.presentation.navigation.WalletTab
+import com.codeskraps.publicpool.presentation.navigation.WorkersTab
 import com.codeskraps.publicpool.ui.theme.PublicPoolTheme
+import org.koin.android.ext.android.inject
 
 class MainActivity : ComponentActivity() {
+    val appReadinessState: AppReadinessState by inject()
+
+    @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
     override fun onCreate(savedInstanceState: Bundle?) {
+        installSplashScreen()
         super.onCreate(savedInstanceState)
         enableEdgeToEdge()
+
+        setupSplashScreenKeepCondition()
+
         setContent {
             PublicPoolTheme {
-                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
-                    Greeting(
-                        name = "Android",
-                        modifier = Modifier.padding(innerPadding)
-                    )
+                TabNavigator(DashboardTab) {
+                    Column(modifier = Modifier.fillMaxSize()) {
+                        Box(modifier = Modifier.weight(1f)) {
+                            CurrentScreen()
+                        }
+                        NavigationBar {
+                            TabNavigationItem(tab = DashboardTab)
+                            TabNavigationItem(tab = WorkersTab)
+                            TabNavigationItem(tab = WalletTab)
+                        }
+                    }
                 }
             }
         }
     }
-}
 
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
-    Text(
-        text = "Hello $name!",
-        modifier = modifier
-    )
+    private fun setupSplashScreenKeepCondition() {
+        val content: View = findViewById(android.R.id.content)
+        content.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    return if (appReadinessState.isReady.value) {
+                        content.viewTreeObserver.removeOnPreDrawListener(this)
+                        true
+                    } else {
+                        false
+                    }
+                }
+            }
+        )
+    }
 }
 
-@Preview(showBackground = true)
 @Composable
-fun GreetingPreview() {
-    PublicPoolTheme {
-        Greeting("Android")
-    }
+private fun RowScope.TabNavigationItem(tab: Tab) {
+    val tabNavigator = LocalTabNavigator.current
+
+    NavigationBarItem(
+        selected = tabNavigator.current == tab,
+        onClick = { tabNavigator.current = tab },
+        icon = {
+            tab.options.icon?.let {
+                Icon(painter = it, contentDescription = tab.options.title)
+            }
+        },
+        label = { Text(text = tab.options.title) }
+    )
 }

+ 29 - 0
app/src/main/java/com/codeskraps/publicpool/MainApplication.kt

@@ -0,0 +1,29 @@
+package com.codeskraps.publicpool // Make sure package matches your project
+
+import android.app.Application
+import com.codeskraps.publicpool.di.appModule
+import com.codeskraps.publicpool.di.dataModule
+import com.codeskraps.publicpool.di.domainModule
+import com.codeskraps.publicpool.di.presentationModule
+import org.koin.android.ext.koin.androidContext
+import org.koin.android.ext.koin.androidLogger
+import org.koin.core.context.GlobalContext.startKoin
+import org.koin.core.logger.Level
+
+class MainApplication : Application() {
+    override fun onCreate() {
+        super.onCreate()
+
+        startKoin {
+            androidLogger(Level.DEBUG) // Use Level.INFO for release builds
+            androidContext(this@MainApplication)
+            modules(
+                appModule, // General app-level dependencies (like DataStore)
+                dataModule, // Data layer dependencies (API, DB, Repositories)
+                domainModule, // Domain layer dependencies (Use Cases)
+                presentationModule // Add presentation module
+                // Add presentationModule later for ViewModels
+            )
+        }
+    }
+} 

+ 7 - 0
app/src/main/java/com/codeskraps/publicpool/data/local/PreferencesKeys.kt

@@ -0,0 +1,7 @@
+package com.codeskraps.publicpool.data.local
+
+import androidx.datastore.preferences.core.stringPreferencesKey
+
+object PreferencesKeys {
+    val WALLET_ADDRESS = stringPreferencesKey("wallet_address")
+} 

+ 118 - 0
app/src/main/java/com/codeskraps/publicpool/data/mappers/Mappers.kt

@@ -0,0 +1,118 @@
+package com.codeskraps.publicpool.data.mappers
+
+import com.codeskraps.publicpool.data.remote.dto.BinanceTickerDto
+import com.codeskraps.publicpool.data.remote.dto.ChartDataPointDto
+import com.codeskraps.publicpool.data.remote.dto.ClientInfoDto
+import com.codeskraps.publicpool.data.remote.dto.NetworkInfoDto
+import com.codeskraps.publicpool.data.remote.dto.TransactionDto
+import com.codeskraps.publicpool.data.remote.dto.WalletInfoDto
+import com.codeskraps.publicpool.data.remote.dto.WorkerDto
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+import com.codeskraps.publicpool.domain.model.ClientInfo
+import com.codeskraps.publicpool.domain.model.CryptoPrice
+import com.codeskraps.publicpool.domain.model.NetworkInfo
+import com.codeskraps.publicpool.domain.model.WalletInfo
+import com.codeskraps.publicpool.domain.model.WalletTransaction
+import com.codeskraps.publicpool.domain.model.Worker
+import com.codeskraps.publicpool.domain.model.toOffsetDateTime
+import kotlinx.serialization.json.Json
+import java.time.OffsetDateTime
+import java.time.format.DateTimeParseException
+
+// Inject or provide Json instance used by Ktor
+// For simplicity here, create a default one. Ensure it matches Ktor's config (ignoreUnknownKeys=true)
+private val jsonParser = Json { ignoreUnknownKeys = true }
+
+// --- DTO to Domain Mappers ---
+
+fun NetworkInfoDto.toDomain(): NetworkInfo {
+    return NetworkInfo(
+        networkDifficulty = this.difficulty ?: 0.0,
+        networkHashRate = this.networkHashPS ?: 0.0,
+        blockHeight = this.blocks ?: 0L,
+        blockWeight = this.currentBlockWeight ?: 0L
+    )
+}
+
+fun WorkerDto.toDomain(): Worker {
+    return Worker(
+        id = this.name ?: "Unknown Worker",
+        sessionId = this.sessionId,
+        bestDifficulty = this.bestDifficulty?.toDoubleOrNull(),
+        hashRate = this.hashRate?.toDoubleOrNull(),
+        startTime = this.startTime,
+        lastSeen = this.lastSeen
+    )
+}
+
+fun ClientInfoDto.toDomain(): ClientInfo {
+    return ClientInfo(
+        // Handle potential conversion or formatting for difficulty later if needed
+        bestDifficulty = this.bestDifficulty ?: "0",
+        workersCount = this.workersCount ?: 0,
+        workers = this.workers?.map { it.toDomain() } ?: emptyList()
+    )
+}
+
+fun ChartDataPointDto.toDomain(): ChartDataPoint? { // Return nullable if parsing fails
+    return try {
+        ChartDataPoint(
+            timestamp = OffsetDateTime.parse(this.label), // Parse ISO 8601 string
+            hashRate = this.data.toDoubleOrNull() ?: 0.0 // Convert string data to Double
+        )
+    } catch (e: DateTimeParseException) {
+        // Log error or handle invalid date format
+        null
+    } catch (e: NumberFormatException) {
+        // Log error or handle invalid number format
+        null
+    }
+}
+
+// Helper to map a list, filtering out nulls from failed conversions
+fun List<ChartDataPointDto>.toDomainList(): List<ChartDataPoint> {
+    return this.mapNotNull { it.toDomain() }
+}
+
+// --- Mappers for blockchain.info ---
+
+fun TransactionDto.toDomain(): WalletTransaction {
+    return WalletTransaction(
+        hash = this.hash,
+        time = this.time.toOffsetDateTime(), // Convert Unix timestamp
+        resultSatoshis = this.result ?: 0L,
+        feeSatoshis = this.fee ?: 0L
+    )
+}
+
+fun WalletInfoDto.toDomain(): WalletInfo {
+    return WalletInfo(
+        address = this.address,
+        finalBalanceSatoshis = this.finalBalance ?: 0L,
+        totalReceivedSatoshis = this.totalReceived ?: 0L,
+        totalSentSatoshis = this.totalSent ?: 0L,
+        transactionCount = this.nTx ?: 0L,
+        transactions = this.txs?.map { it.toDomain() } ?: emptyList()
+    )
+}
+
+// --- Mapper for Binance Ticker ---
+
+fun BinanceTickerDto.toCryptoPrice(currency: String = "USD"): CryptoPrice? {
+    return try {
+        val priceValue = this.price.toDoubleOrNull() ?: return null
+
+        // Extract base symbol (e.g., BTC from BTCUSDT)
+        val baseSymbol = this.symbol.removeSuffix(currency)
+
+        CryptoPrice(
+            symbol = baseSymbol,
+            price = priceValue,
+            currency = currency,
+            lastUpdated = OffsetDateTime.now() // Binance API doesn't provide timestamp here
+        )
+    } catch (e: Exception) {
+        // Log error
+        null
+    }
+} 

+ 65 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/KtorApiService.kt

@@ -0,0 +1,65 @@
+package com.codeskraps.publicpool.data.remote
+
+import com.codeskraps.publicpool.data.remote.dto.ChartDataPointDto
+// No longer need ClientChartResponseDto for this call
+// import com.codeskraps.publicpool.data.remote.dto.ClientChartResponseDto
+import com.codeskraps.publicpool.data.remote.dto.ClientInfoDto
+import com.codeskraps.publicpool.data.remote.dto.NetworkInfoDto
+import com.codeskraps.publicpool.data.remote.dto.WalletInfoDto
+import com.codeskraps.publicpool.data.remote.dto.BinanceTickerDto // Import Binance DTO
+import io.ktor.client.* // Ktor client
+import io.ktor.client.call.* // body()
+import io.ktor.client.request.* // get()
+
+interface KtorApiService {
+    suspend fun getClientInfo(walletAddress: String): ClientInfoDto
+    suspend fun getNetworkInfo(): NetworkInfoDto
+    // Ensure interface matches implementation return type
+    suspend fun getChartData(walletAddress: String): List<ChartDataPointDto>
+
+    // Add new function for blockchain.info
+    suspend fun getBlockchainWalletInfo(walletAddress: String): WalletInfoDto
+
+    // Update to Binance endpoint
+    suspend fun getTickerPrice(symbol: String = "BTCUSDT"): BinanceTickerDto
+
+    // We can add implementations here or in a separate class
+    companion object {
+        const val BASE_URL = "https://public-pool.io:40557/api" // Define base URL
+        const val BLOCKCHAIN_INFO_BASE_URL = "https://blockchain.info" // New base URL
+        const val BINANCE_BASE_URL = "https://api.binance.com" // Binance base URL
+    }
+}
+
+// Example Implementation (can be provided via Koin later)
+class KtorApiServiceImpl(private val client: HttpClient) : KtorApiService {
+
+    override suspend fun getClientInfo(walletAddress: String): ClientInfoDto {
+        return client.get("${KtorApiService.BASE_URL}/client/$walletAddress").body()
+    }
+
+    override suspend fun getNetworkInfo(): NetworkInfoDto {
+        return client.get("${KtorApiService.BASE_URL}/network").body()
+    }
+
+    override suspend fun getChartData(walletAddress: String): List<ChartDataPointDto> {
+        // Use the correct URL with /chart and expect a List directly
+        return client.get("${KtorApiService.BASE_URL}/client/$walletAddress/chart").body<List<ChartDataPointDto>>()
+        // Or let type inference work:
+        // return client.get("${KtorApiService.BASE_URL}/client/$walletAddress/chart").body()
+    }
+
+    // Implementation for new function
+    override suspend fun getBlockchainWalletInfo(walletAddress: String): WalletInfoDto {
+        return client.get("${KtorApiService.BLOCKCHAIN_INFO_BASE_URL}/address/$walletAddress") {
+            parameter("format", "json") // Add format=json parameter
+        }.body()
+    }
+
+    // Implementation for Binance endpoint
+    override suspend fun getTickerPrice(symbol: String): BinanceTickerDto {
+        return client.get("${KtorApiService.BINANCE_BASE_URL}/api/v3/ticker/price") {
+            parameter("symbol", symbol)
+        }.body()
+    }
+} 

+ 11 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/dto/BinanceTickerDto.kt

@@ -0,0 +1,11 @@
+package com.codeskraps.publicpool.data.remote.dto
+
+import kotlinx.serialization.Serializable
+
+// --- DTO for Binance Ticker Price ---
+
+@Serializable
+data class BinanceTickerDto(
+    val symbol: String, // e.g., "BTCUSDT"
+    val price: String // Price is returned as a String
+) 

+ 79 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/dto/BlockchainInfoDtos.kt

@@ -0,0 +1,79 @@
+package com.codeskraps.publicpool.data.remote.dto
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+// --- DTOs for blockchain.info API ---
+
+@Serializable
+data class WalletInfoDto(
+    val hash160: String? = null,
+    val address: String,
+    @SerialName("n_tx") val nTx: Long? = null,
+    @SerialName("n_unredeemed") val nUnredeemed: Long? = null,
+    @SerialName("total_received") val totalReceived: Long? = null,
+    @SerialName("total_sent") val totalSent: Long? = null,
+    @SerialName("final_balance") val finalBalance: Long? = null,
+    val txs: List<TransactionDto>? = null
+)
+
+@Serializable
+data class TransactionDto(
+    val hash: String,
+    val ver: Int? = null,
+    @SerialName("vin_sz") val vinSz: Int? = null,
+    @SerialName("vout_sz") val voutSz: Int? = null,
+    val size: Long? = null,
+    val weight: Long? = null,
+    val fee: Long? = null,
+    @SerialName("relayed_by") val relayedBy: String? = null,
+    @SerialName("lock_time") val lockTime: Long? = null,
+    @SerialName("tx_index") val txIndex: Long? = null,
+    @SerialName("double_spend") val doubleSpend: Boolean? = null,
+    val time: Long? = null, // Unix timestamp
+    @SerialName("block_index") val blockIndex: Long? = null,
+    @SerialName("block_height") val blockHeight: Long? = null,
+    val inputs: List<InputDto>? = null,
+    val out: List<OutputDto>? = null,
+    val result: Long? = null, // Net result of tx for this address (in satoshis)
+    val balance: Long? = null // Balance after this tx (in satoshis)
+)
+
+@Serializable
+data class InputDto(
+    val sequence: Long? = null,
+    val witness: String? = null,
+    val script: String? = null,
+    val index: Int? = null,
+    @SerialName("prev_out") val prevOut: PrevOutDto? = null
+)
+
+@Serializable
+data class OutputDto(
+    val type: Int? = null,
+    val spent: Boolean? = null,
+    val value: Long? = null, // Value in satoshis
+    @SerialName("spending_outpoints") val spendingOutpoints: List<SpendingOutpointDto>? = null,
+    val n: Int? = null,
+    @SerialName("tx_index") val txIndex: Long? = null,
+    val script: String? = null,
+    val addr: String? = null
+)
+
+@Serializable
+data class PrevOutDto(
+    val type: Int? = null,
+    val spent: Boolean? = null,
+    val value: Long? = null, // Value in satoshis
+    @SerialName("spending_outpoints") val spendingOutpoints: List<SpendingOutpointDto>? = null,
+    val n: Int? = null,
+    @SerialName("tx_index") val txIndex: Long? = null,
+    val script: String? = null,
+    val addr: String? = null
+)
+
+@Serializable
+data class SpendingOutpointDto(
+    @SerialName("tx_index") val txIndex: Long? = null,
+    val n: Int? = null
+) 

+ 13 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/dto/ChartDataPointDto.kt

@@ -0,0 +1,13 @@
+package com.codeskraps.publicpool.data.remote.dto
+
+import kotlinx.serialization.Serializable
+
+// API returns a list directly, so we define the item structure
+@Serializable
+data class ChartDataPointDto(
+    val label: String, // ISO 8601 date-time string
+    val data: String // Hash rate as a string, needs conversion
+)
+
+// We can use a typealias if needed, but Ktor can handle List<ChartDataPointDto> directly
+// typealias ChartDataDto = List<ChartDataPointDto> 

+ 10 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/dto/ClientInfoDto.kt

@@ -0,0 +1,10 @@
+package com.codeskraps.publicpool.data.remote.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class ClientInfoDto(
+    val bestDifficulty: String?, // Can be null if no data yet? API seems to send "0" sometimes
+    val workersCount: Int?,
+    val workers: List<WorkerDto>? // Define WorkerDto if needed, empty in example
+)

+ 16 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/dto/NetworkInfoDto.kt

@@ -0,0 +1,16 @@
+package com.codeskraps.publicpool.data.remote.dto
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class NetworkInfoDto(
+    val blocks: Long?,
+    @SerialName("currentblockweight") val currentBlockWeight: Long?,
+    @SerialName("currentblocktx") val currentBlockTx: Long?,
+    val difficulty: Double?, // Using Double as it seems to be a large number
+    @SerialName("networkhashps") val networkHashPS: Double?, // Using Double for large hash rate value
+    @SerialName("pooledtx") val pooledTx: Long?,
+    val chain: String?,
+    val warnings: List<String>?
+) 

+ 13 - 0
app/src/main/java/com/codeskraps/publicpool/data/remote/dto/WorkerDto.kt

@@ -0,0 +1,13 @@
+package com.codeskraps.publicpool.data.remote.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class WorkerDto(
+    val sessionId: String? = null,
+    val name: String? = null,
+    val bestDifficulty: String? = null, // Difficulty as String
+    val hashRate: String? = null, // Hash rate as String
+    val startTime: String? = null, // ISO 8601 date-time string
+    val lastSeen: String? = null // ISO 8601 date-time string
+) 

+ 124 - 0
app/src/main/java/com/codeskraps/publicpool/data/repository/PublicPoolRepositoryImpl.kt

@@ -0,0 +1,124 @@
+package com.codeskraps.publicpool.data.repository
+
+import android.util.Log
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.emptyPreferences
+import com.codeskraps.publicpool.data.local.PreferencesKeys
+import com.codeskraps.publicpool.data.remote.KtorApiService
+import com.codeskraps.publicpool.data.mappers.toDomain
+import com.codeskraps.publicpool.data.mappers.toDomainList
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+import com.codeskraps.publicpool.domain.model.ClientInfo
+import com.codeskraps.publicpool.domain.model.NetworkInfo
+import com.codeskraps.publicpool.domain.model.WalletInfo
+import com.codeskraps.publicpool.domain.model.CryptoPrice
+import com.codeskraps.publicpool.data.mappers.toCryptoPrice
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.map
+import java.io.IOException
+
+class PublicPoolRepositoryImpl(
+    private val apiService: KtorApiService,
+    private val dataStore: DataStore<Preferences>
+) : PublicPoolRepository {
+
+    companion object {
+        private const val TAG = "PublicPoolRepository" // Tag for logging
+    }
+
+    // --- Network Data ---
+    override suspend fun getNetworkInfo(): Result<NetworkInfo> {
+        return try {
+            val networkInfoDto = apiService.getNetworkInfo()
+            Result.success(networkInfoDto.toDomain()) // Map DTO to Domain
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to get network info", e)
+            Result.failure(e)
+        }
+    }
+
+    // --- Client Data ---
+    override suspend fun getClientInfo(walletAddress: String): Result<ClientInfo> {
+        return try {
+            val clientInfoDto = apiService.getClientInfo(walletAddress)
+            Result.success(clientInfoDto.toDomain())
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to get client info for $walletAddress", e)
+            Result.failure(e)
+        }
+    }
+
+    override suspend fun getChartData(walletAddress: String): Result<List<ChartDataPoint>> {
+        return try {
+            val chartDataDtoList = apiService.getChartData(walletAddress)
+            Result.success(chartDataDtoList.toDomainList()) // Map list of DTOs
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to get chart data for $walletAddress", e)
+            Result.failure(e)
+        }
+    }
+
+    // --- Wallet Address Management (DataStore) ---
+    override fun getWalletAddress(): Flow<String?> {
+        return dataStore.data
+            .catch { exception ->
+                // dataStore.data throws an IOException if it can't read the data
+                if (exception is IOException) {
+                    Log.e(TAG, "Error reading wallet address from DataStore", exception)
+                    emit(emptyPreferences()) // Emit empty preferences on error
+                } else {
+                    throw exception // Rethrow other exceptions
+                }
+            }
+            .map { preferences ->
+                preferences[PreferencesKeys.WALLET_ADDRESS]
+            }
+    }
+
+    override suspend fun saveWalletAddress(address: String) {
+       try {
+            dataStore.edit { preferences ->
+                preferences[PreferencesKeys.WALLET_ADDRESS] = address
+            }
+       } catch(e: Exception) {
+            Log.e(TAG, "Failed to save wallet address to DataStore", e)
+            // Optionally rethrow or handle the error based on requirements (e.g., return Result<Unit>)
+            // For now, just log the error.
+       }
+    }
+
+    // --- Blockchain.info Data ---
+    override suspend fun getBlockchainWalletInfo(walletAddress: String): Result<WalletInfo> {
+        return try {
+            val walletInfoDto = apiService.getBlockchainWalletInfo(walletAddress)
+            Result.success(walletInfoDto.toDomain()) // Map DTO to Domain
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to get blockchain wallet info for $walletAddress", e)
+            Result.failure(e)
+        }
+    }
+
+    // --- Price Data ---
+    override suspend fun getBtcPriceUsdt(): Result<CryptoPrice> {
+        return try {
+            // Call the service function (defaults to BTCUSDT)
+            val response = apiService.getTickerPrice()
+            // Correct: Pass the QUOTE currency ("USDT") to the mapper
+            val cryptoPrice = response.toCryptoPrice(currency = "USDT")
+            if (cryptoPrice != null) {
+                Result.success(cryptoPrice)
+            } else {
+                // Correct: Simple error message for Binance parsing failure
+                Result.failure(Exception("Failed to parse BTC price from Binance response."))
+            }
+        } catch (e: Exception) {
+            // Correct: Log message should mention Binance
+            Log.e(TAG, "Failed to get BTC price from Binance", e)
+            Result.failure(e)
+        }
+    }
+}

+ 22 - 0
app/src/main/java/com/codeskraps/publicpool/di/AppModule.kt

@@ -0,0 +1,22 @@
+package com.codeskraps.publicpool.di
+
+import android.content.Context
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.preferencesDataStore
+import org.koin.android.ext.koin.androidContext
+import org.koin.dsl.module
+
+// Define DataStore instance at the top level
+// Use a unique name for the DataStore file
+val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "public_pool_settings")
+
+val appModule = module {
+    // Provide DataStore instance
+    single<DataStore<Preferences>> {
+        androidContext().dataStore
+    }
+
+    // Provide AppReadinessState as a singleton
+    single { AppReadinessState() }
+} 

+ 14 - 0
app/src/main/java/com/codeskraps/publicpool/di/AppReadinessState.kt

@@ -0,0 +1,14 @@
+package com.codeskraps.publicpool.di
+
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+// Simple state holder to signal when initial data is ready for splash screen dismissal
+class AppReadinessState {
+    private val _isReady = MutableStateFlow(false)
+    val isReady = _isReady.asStateFlow()
+
+    fun setReady() {
+        _isReady.value = true
+    }
+} 

+ 44 - 0
app/src/main/java/com/codeskraps/publicpool/di/DataModule.kt

@@ -0,0 +1,44 @@
+package com.codeskraps.publicpool.di
+
+import com.codeskraps.publicpool.data.remote.KtorApiService
+import com.codeskraps.publicpool.data.remote.KtorApiServiceImpl
+import com.codeskraps.publicpool.data.repository.PublicPoolRepositoryImpl
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+import io.ktor.client.* // Ktor client
+import io.ktor.client.engine.android.* // Ktor Android engine
+import io.ktor.client.plugins.contentnegotiation.* // Ktor Content Negotiation
+import io.ktor.client.plugins.logging.* // Ktor Logging
+import io.ktor.serialization.kotlinx.json.* // Ktor Kotlinx Serialization
+import kotlinx.serialization.json.Json
+import org.koin.dsl.module
+
+val dataModule = module {
+
+    // Ktor HTTP Client
+    single<HttpClient> {
+        HttpClient(Android) { // Or CIO, OkHttp if preferred and added dependency
+            expectSuccess = true // Optional: Throw exception for non-2xx responses
+
+            // Logging
+            install(Logging) {
+                logger = Logger.DEFAULT // Simple logger
+                level = LogLevel.ALL // Log everything during development
+            }
+
+            // JSON Serialization/Deserialization
+            install(ContentNegotiation) {
+                json(Json {
+                    prettyPrint = true
+                    isLenient = true
+                    ignoreUnknownKeys = true // Important for API changes
+                })
+            }
+        }
+    }
+
+    // API Service Implementation
+    single<KtorApiService> { KtorApiServiceImpl(client = get()) }
+
+    // Repository Implementation
+    single<PublicPoolRepository> { PublicPoolRepositoryImpl(apiService = get(), dataStore = get()) }
+} 

+ 16 - 0
app/src/main/java/com/codeskraps/publicpool/di/DomainModule.kt

@@ -0,0 +1,16 @@
+package com.codeskraps.publicpool.di
+
+import com.codeskraps.publicpool.domain.usecase.*
+import org.koin.dsl.module
+
+val domainModule = module {
+    // Use Case providers
+    factory { GetWalletAddressUseCase(repository = get()) }
+    factory { SaveWalletAddressUseCase(repository = get()) }
+    factory { GetNetworkInfoUseCase(repository = get()) }
+    factory { GetClientInfoUseCase(repository = get()) }
+    factory { GetChartDataUseCase(repository = get()) }
+    factory { CalculateTwoHourAverageUseCase() }
+    factory { GetBlockchainWalletInfoUseCase(repository = get()) }
+    factory { GetBtcPriceUseCase(repository = get()) }
+} 

+ 15 - 0
app/src/main/java/com/codeskraps/publicpool/di/PresentationModule.kt

@@ -0,0 +1,15 @@
+package com.codeskraps.publicpool.di
+
+import com.codeskraps.publicpool.presentation.dashboard.DashboardScreenModel
+import com.codeskraps.publicpool.presentation.settings.SettingsScreenModel
+import com.codeskraps.publicpool.presentation.wallet.WalletScreenModel // Import Wallet ScreenModel
+import com.codeskraps.publicpool.presentation.workers.WorkersScreenModel // Import Worker ScreenModel
+import org.koin.dsl.module
+
+val presentationModule = module {
+    // Voyager ScreenModels (similar to ViewModels)
+    factory { DashboardScreenModel(get(), get(), get(), get(), get(), get()) } // Add 6th `get()` for AppReadinessState
+    factory { SettingsScreenModel(get(), get()) } // Inject use cases
+    factory { WorkersScreenModel(get(), get()) } // Provide WorkersScreenModel
+    factory { WalletScreenModel(get(), get(), get()) } // Provide WalletScreenModel (add 3rd get)
+} 

+ 44 - 0
app/src/main/java/com/codeskraps/publicpool/domain/model/BlockchainInfoModels.kt

@@ -0,0 +1,44 @@
+package com.codeskraps.publicpool.domain.model
+
+import java.time.Instant
+import java.time.OffsetDateTime
+import java.time.ZoneId
+
+// Represents overall wallet summary
+data class WalletInfo(
+    val address: String,
+    val finalBalanceSatoshis: Long,
+    val totalReceivedSatoshis: Long,
+    val totalSentSatoshis: Long,
+    val transactionCount: Long,
+    val transactions: List<WalletTransaction> // Include simplified transaction list
+) {
+    // Helper to convert satoshis to BTC
+    val finalBalanceBtc: Double
+        get() = finalBalanceSatoshis / 100_000_000.0
+    val totalReceivedBtc: Double
+        get() = totalReceivedSatoshis / 100_000_000.0
+    val totalSentBtc: Double
+        get() = totalSentSatoshis / 100_000_000.0
+}
+
+// Represents a single simplified transaction for display
+data class WalletTransaction(
+    val hash: String,
+    val time: OffsetDateTime?, // Parsed time
+    val resultSatoshis: Long, // Net change for this address
+    val feeSatoshis: Long
+) {
+    val resultBtc: Double
+        get() = resultSatoshis / 100_000_000.0
+}
+
+// Helper function to convert Unix timestamp (seconds) to OffsetDateTime
+fun Long?.toOffsetDateTime(): OffsetDateTime? {
+    if (this == null) return null
+    return try {
+        OffsetDateTime.ofInstant(Instant.ofEpochSecond(this), ZoneId.systemDefault())
+    } catch (e: Exception) {
+        null // Handle potential parsing errors
+    }
+} 

+ 8 - 0
app/src/main/java/com/codeskraps/publicpool/domain/model/ChartDataPoint.kt

@@ -0,0 +1,8 @@
+package com.codeskraps.publicpool.domain.model
+
+import java.time.OffsetDateTime // Use java.time for better date handling
+
+data class ChartDataPoint(
+    val timestamp: OffsetDateTime, // Parsed timestamp
+    val hashRate: Double // Converted hash rate
+) 

+ 7 - 0
app/src/main/java/com/codeskraps/publicpool/domain/model/ClientInfo.kt

@@ -0,0 +1,7 @@
+package com.codeskraps.publicpool.domain.model
+
+data class ClientInfo(
+    val bestDifficulty: String, // Keep as String for display formatting? Or convert?
+    val workersCount: Int,
+    val workers: List<Worker> // Assuming Worker is a data class defined elsewhere
+) 

+ 10 - 0
app/src/main/java/com/codeskraps/publicpool/domain/model/CryptoPrice.kt

@@ -0,0 +1,10 @@
+package com.codeskraps.publicpool.domain.model
+
+import java.time.OffsetDateTime
+
+data class CryptoPrice(
+    val symbol: String,
+    val price: Double,
+    val currency: String, // e.g., "USD"
+    val lastUpdated: OffsetDateTime? // When the price was last updated
+) 

+ 8 - 0
app/src/main/java/com/codeskraps/publicpool/domain/model/NetworkInfo.kt

@@ -0,0 +1,8 @@
+package com.codeskraps.publicpool.domain.model
+
+data class NetworkInfo(
+    val networkDifficulty: Double,
+    val networkHashRate: Double,
+    val blockHeight: Long,
+    val blockWeight: Long // Added from DTO
+) 

+ 10 - 0
app/src/main/java/com/codeskraps/publicpool/domain/model/Worker.kt

@@ -0,0 +1,10 @@
+package com.codeskraps.publicpool.domain.model
+
+data class Worker(
+    val id: String, // Using 'name' as the unique ID
+    val sessionId: String?,
+    val bestDifficulty: Double?,
+    val hashRate: Double?,
+    val startTime: String?,
+    val lastSeen: String?
+) 

+ 28 - 0
app/src/main/java/com/codeskraps/publicpool/domain/repository/PublicPoolRepository.kt

@@ -0,0 +1,28 @@
+package com.codeskraps.publicpool.domain.repository
+
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+import com.codeskraps.publicpool.domain.model.ClientInfo
+import com.codeskraps.publicpool.domain.model.NetworkInfo
+import com.codeskraps.publicpool.domain.model.WalletInfo
+import com.codeskraps.publicpool.domain.model.CryptoPrice
+import kotlinx.coroutines.flow.Flow
+
+interface PublicPoolRepository {
+
+    // --- Network Data ---
+    suspend fun getNetworkInfo(): Result<NetworkInfo> // Use Result for error handling
+
+    // --- Client Data ---
+    suspend fun getClientInfo(walletAddress: String): Result<ClientInfo>
+    suspend fun getChartData(walletAddress: String): Result<List<ChartDataPoint>>
+
+    // --- Wallet Address Management (DataStore) ---
+    fun getWalletAddress(): Flow<String?> // Flow to observe changes
+    suspend fun saveWalletAddress(address: String)
+
+    // --- Blockchain.info Data ---
+    suspend fun getBlockchainWalletInfo(walletAddress: String): Result<WalletInfo>
+
+    // --- Price Data ---
+    suspend fun getBtcPriceUsdt(): Result<CryptoPrice>
+} 

+ 44 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/CalculateTwoHourAverageUseCase.kt

@@ -0,0 +1,44 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+
+class CalculateTwoHourAverageUseCase { // Allow Koin/Hilt injection
+
+    // Define the window size (2 hours = 12 * 10 minutes)
+    private val windowSize = 12
+
+    operator fun invoke(tenMinuteData: List<ChartDataPoint>): List<ChartDataPoint> {
+        if (tenMinuteData.size < windowSize) {
+            // Not enough data to calculate a full 2-hour average
+            return emptyList()
+        }
+
+        // Ensure data is sorted by timestamp, although API seems to provide it sorted
+        val sortedData = tenMinuteData.sortedBy { it.timestamp }
+
+        val twoHourAverageData = mutableListOf<ChartDataPoint>()
+
+        // Calculate the rolling average
+        for (i in (windowSize - 1) until sortedData.size) {
+            // Get the window of points (current point + previous 11)
+            val window = sortedData.subList(i - windowSize + 1, i + 1)
+
+            // Calculate the average hash rate for the window
+            val averageHashRate = window.map { it.hashRate }.average()
+
+            // Use the timestamp of the *last* point in the window for the averaged point
+            val timestamp = sortedData[i].timestamp
+
+            if (!averageHashRate.isNaN()) { // Avoid adding NaN if window is empty (shouldn't happen here)
+                 twoHourAverageData.add(
+                    ChartDataPoint(
+                        timestamp = timestamp,
+                        hashRate = averageHashRate
+                    )
+                )
+            }
+        }
+
+        return twoHourAverageData
+    }
+} 

+ 15 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetBlockchainWalletInfoUseCase.kt

@@ -0,0 +1,15 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.model.WalletInfo
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+
+class GetBlockchainWalletInfoUseCase(
+    private val repository: PublicPoolRepository
+) {
+    suspend operator fun invoke(walletAddress: String): Result<WalletInfo> {
+        if (walletAddress.isBlank()) {
+            return Result.failure(IllegalArgumentException("Wallet address cannot be blank"))
+        }
+        return repository.getBlockchainWalletInfo(walletAddress)
+    }
+} 

+ 13 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetBtcPriceUseCase.kt

@@ -0,0 +1,13 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.model.CryptoPrice
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+
+class GetBtcPriceUseCase(
+    private val repository: PublicPoolRepository
+) {
+    // Default currency can be USD, or allow specifying
+    suspend operator fun invoke(): Result<CryptoPrice> {
+        return repository.getBtcPriceUsdt()
+    }
+} 

+ 13 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetChartDataUseCase.kt

@@ -0,0 +1,13 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+
+class GetChartDataUseCase(private val repository: PublicPoolRepository) {
+    suspend operator fun invoke(walletAddress: String): Result<List<ChartDataPoint>> {
+        if (walletAddress.isBlank()) {
+            return Result.failure(IllegalArgumentException("Wallet address cannot be blank"))
+        }
+        return repository.getChartData(walletAddress)
+    }
+} 

+ 13 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetClientInfoUseCase.kt

@@ -0,0 +1,13 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.model.ClientInfo
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+
+class GetClientInfoUseCase(private val repository: PublicPoolRepository) {
+    suspend operator fun invoke(walletAddress: String): Result<ClientInfo> {
+        if (walletAddress.isBlank()) {
+            return Result.failure(IllegalArgumentException("Wallet address cannot be blank"))
+        }
+        return repository.getClientInfo(walletAddress)
+    }
+} 

+ 10 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetNetworkInfoUseCase.kt

@@ -0,0 +1,10 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.model.NetworkInfo
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+
+class GetNetworkInfoUseCase(private val repository: PublicPoolRepository) {
+    suspend operator fun invoke(): Result<NetworkInfo> {
+        return repository.getNetworkInfo()
+    }
+} 

+ 10 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/GetWalletAddressUseCase.kt

@@ -0,0 +1,10 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+import kotlinx.coroutines.flow.Flow
+
+class GetWalletAddressUseCase(private val repository: PublicPoolRepository) {
+    operator fun invoke(): Flow<String?> {
+        return repository.getWalletAddress()
+    }
+} 

+ 12 - 0
app/src/main/java/com/codeskraps/publicpool/domain/usecase/SaveWalletAddressUseCase.kt

@@ -0,0 +1,12 @@
+package com.codeskraps.publicpool.domain.usecase
+
+import com.codeskraps.publicpool.domain.repository.PublicPoolRepository
+
+class SaveWalletAddressUseCase(private val repository: PublicPoolRepository) {
+    suspend operator fun invoke(address: String) {
+        // Add validation logic here if needed before saving
+        if (address.isNotBlank()) { // Basic validation
+            repository.saveWalletAddress(address.trim())
+        }
+    }
+} 

+ 33 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/common/AppCard.kt

@@ -0,0 +1,33 @@
+package com.codeskraps.publicpool.presentation.common
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/**
+ * Base card composable that enforces the app's surface color and border style.
+ */
+@Composable
+fun AppCard(
+    modifier: Modifier = Modifier,
+    shape: Shape = CardDefaults.shape,
+    elevation: Dp = 1.dp, // Default elevation
+    content: @Composable ColumnScope.() -> Unit
+) {
+    Card(
+        modifier = modifier,
+        shape = shape,
+        colors = CardDefaults.cardColors(
+            containerColor = MaterialTheme.colorScheme.surface // Ensure surface color is used
+        ),
+        elevation = CardDefaults.cardElevation(defaultElevation = elevation),
+        // Use the outline color from the theme for the border
+        border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
+        content = content
+    )
+} 

+ 18 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/common/MviBase.kt

@@ -0,0 +1,18 @@
+package com.codeskraps.publicpool.presentation.common
+
+/**
+ * Represents the state of a UI screen.
+ * It should be an immutable data class.
+ */
+interface UiState
+
+/**
+ * Represents user actions or events triggered from the UI.
+ */
+interface UiEvent
+
+/**
+ * Represents side effects that the ViewModel needs to trigger,
+ * such as navigation, showing toasts/snackbars, etc.
+ */
+interface UiEffect 

+ 374 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardContent.kt

@@ -0,0 +1,374 @@
+package com.codeskraps.publicpool.presentation.dashboard
+
+import android.util.Log
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+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
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import com.anychart.APIlib
+import com.anychart.AnyChart
+import com.anychart.AnyChartView
+import com.anychart.chart.common.dataentry.ValueDataEntry
+import com.anychart.charts.Cartesian
+import com.anychart.enums.Anchor
+import com.anychart.enums.MarkerType
+import com.anychart.enums.TooltipPositionMode
+import com.anychart.graphics.vector.Stroke
+import com.codeskraps.publicpool.R
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+import com.codeskraps.publicpool.presentation.common.AppCard
+import com.codeskraps.publicpool.presentation.navigation.SettingsScreen
+import com.codeskraps.publicpool.ui.theme.PositiveGreen
+import com.codeskraps.publicpool.util.formatHashRate
+import com.codeskraps.publicpool.util.formatLargeNumber
+import kotlinx.coroutines.flow.collectLatest
+import java.text.NumberFormat
+import java.util.Locale
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun DashboardContent(screenModel: DashboardScreenModel) {
+    val state by screenModel.state.collectAsState()
+    val navigator = LocalNavigator.currentOrThrow
+    val snackbarHostState = remember { SnackbarHostState() }
+
+    // Handle effects (navigation, snackbars)
+    LaunchedEffect(key1 = screenModel.effect) {
+        screenModel.effect.collectLatest { effect ->
+            when (effect) {
+                DashboardEffect.NavigateToSettings -> navigator.push(SettingsScreen)
+                is DashboardEffect.ShowErrorSnackbar -> {
+                    snackbarHostState.showSnackbar(
+                        message = effect.message,
+                        duration = SnackbarDuration.Short
+                    )
+                }
+            }
+        }
+    }
+
+    Scaffold(
+        snackbarHost = { SnackbarHost(snackbarHostState) },
+        topBar = {
+            TopAppBar(
+                title = { Text(stringResource(R.string.screen_title_dashboard)) },
+                actions = {
+                    // Show loading indicator in TopAppBar if any data is loading
+                    if (state.isLoading) {
+                        CircularProgressIndicator(
+                            modifier = Modifier.size(24.dp),
+                            strokeWidth = 2.dp
+                        )
+                        Spacer(Modifier.width(8.dp))
+                    }
+                    IconButton(onClick = { screenModel.handleEvent(DashboardEvent.GoToSettings) }) {
+                        Icon(Icons.Filled.Settings, contentDescription = stringResource(R.string.dashboard_action_settings))
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(paddingValues)
+                .padding(horizontal = 16.dp) // Add horizontal padding
+                .verticalScroll(rememberScrollState()) // Make column scrollable
+        ) {
+            // Add padding between TopAppBar and first card row
+            Spacer(modifier = Modifier.height(16.dp))
+
+            // Show message if no wallet address is set
+            if (!state.isWalletLoading && state.walletAddress.isNullOrBlank()) {
+                AppCard(modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp)) {
+                    Text(
+                        text = stringResource(R.string.dashboard_info_set_wallet),
+                        modifier = Modifier.padding(16.dp),
+                        style = MaterialTheme.typography.bodyLarge
+                    )
+                }
+            }
+
+            // Top Info Cards Row/Grid
+            TopInfoCards(state = state)
+
+            Spacer(modifier = Modifier.height(16.dp))
+
+            // Placeholder for Workers List (Add later if API provides worker data)
+            // WorkersSection(state = state)
+
+            // Chart Section
+            ChartSection(state = state)
+
+            Spacer(modifier = Modifier.height(16.dp)) // Bottom padding
+        }
+    }
+}
+
+@Composable
+fun TopInfoCards(state: DashboardState) {
+    val numberFormat = remember { NumberFormat.getNumberInstance(Locale.US) }
+
+    // Using Row with weights for responsiveness, consider Grid for more items
+    Row(modifier = Modifier.fillMaxWidth()) {
+        InfoCard(
+            label = stringResource(R.string.dashboard_card_label_your_best_difficulty),
+            value = state.clientInfo?.bestDifficulty?.toDoubleOrNull()?.let { formatLargeNumber(it) } ?: state.clientInfo?.bestDifficulty ?: stringResource(R.string.text_placeholder_dash),
+            secondaryValue = state.clientInfo?.bestDifficulty?.toDoubleOrNull()?.let { numberFormat.format(it) },
+            isLoading = state.isClientInfoLoading,
+            modifier = Modifier.weight(1f)
+        )
+        Spacer(modifier = Modifier.width(8.dp))
+        InfoCard(
+            label = stringResource(R.string.dashboard_card_label_network_difficulty),
+            value = formatLargeNumber(state.networkInfo?.networkDifficulty ?: 0.0),
+            secondaryValue = numberFormat.format(state.networkInfo?.networkDifficulty ?: 0.0),
+            isLoading = state.isNetworkLoading,
+            modifier = Modifier.weight(1f)
+        )
+    }
+    Spacer(modifier = Modifier.height(8.dp))
+    Row(modifier = Modifier.fillMaxWidth()) {
+        InfoCard(
+            label = stringResource(R.string.dashboard_card_label_network_hash_rate),
+            value = formatHashRate(state.networkInfo?.networkHashRate ?: 0.0),
+            isLoading = state.isNetworkLoading,
+            modifier = Modifier.weight(1f)
+        )
+        Spacer(modifier = Modifier.width(8.dp))
+        InfoCard(
+            label = stringResource(R.string.dashboard_card_label_block_height),
+            value = numberFormat.format(state.networkInfo?.blockHeight ?: 0L),
+            secondaryValue = "${stringResource(R.string.dashboard_card_secondary_block_weight_prefix)} ${numberFormat.format(state.networkInfo?.blockWeight ?: 0L)}",
+            isLoading = state.isNetworkLoading,
+            modifier = Modifier.weight(1f)
+        )
+    }
+}
+
+@Composable
+fun InfoCard(
+    label: String,
+    value: String,
+    isLoading: Boolean,
+    modifier: Modifier = Modifier,
+    secondaryValue: String? = null
+) {
+    AppCard(modifier = modifier) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                 // Ensure consistent height regardless of secondary text
+                .defaultMinSize(minHeight = 90.dp) // Adjusted minHeight slightly
+                .padding(12.dp),
+             verticalArrangement = Arrangement.SpaceBetween
+        ) {
+            Text(text = label, style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+            Box(modifier = Modifier.align(Alignment.End)) {
+                if (isLoading) {
+                    CircularProgressIndicator(modifier = Modifier.size(20.dp))
+                } else {
+                    Column(horizontalAlignment = Alignment.End) {
+                        Text(
+                            text = value,
+                            style = MaterialTheme.typography.headlineSmall,
+                            fontWeight = FontWeight.Bold,
+                            color = MaterialTheme.colorScheme.onSurface // Explicitly white
+                        )
+                        // Render secondary text OR an empty text with same style for spacing
+                        Text(
+                            text = secondaryValue ?: "", // Display secondary value or empty string
+                            style = MaterialTheme.typography.bodySmall, // Apply same style
+                            color = if (secondaryValue != null) PositiveGreen else Color.Transparent // Use green or make invisible
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun ChartSection(state: DashboardState) {
+    AppCard(modifier = Modifier.fillMaxWidth()) {
+        Column(modifier = Modifier.padding(vertical = 16.dp)) { // Padding only vertical
+            Text(
+                text = stringResource(R.string.dashboard_chart_title),
+                style = MaterialTheme.typography.titleMedium,
+                modifier = Modifier.padding(horizontal = 16.dp)
+            )
+            Spacer(modifier = Modifier.height(8.dp))
+
+            val chartContainerModifier = Modifier
+                .fillMaxWidth()
+                .height(250.dp)
+
+            when {
+                state.isChartDataLoading -> {
+                    Box(modifier = chartContainerModifier, contentAlignment = Alignment.Center) {
+                        CircularProgressIndicator()
+                    }
+                }
+                state.chartData.isNotEmpty() -> {
+                    HashRateChart(
+                        tenMinData = state.chartData,
+                        twoHourData = state.chartDataTwoHourAvg, // Pass 2h data
+                        modifier = chartContainerModifier
+                    )
+                }
+                !state.isWalletLoading && !state.walletAddress.isNullOrBlank() -> {
+                     // Wallet is set, not loading, but chart data is empty
+                     Box(modifier = chartContainerModifier.padding(horizontal = 16.dp), contentAlignment = Alignment.Center) {
+                        Text(stringResource(R.string.dashboard_chart_no_data))
+                    }
+                }
+                else -> {
+                    // Placeholder when wallet isn't set or still loading wallet
+                     Box(modifier = chartContainerModifier.padding(horizontal = 16.dp), contentAlignment = Alignment.Center) {
+                        // Avoid showing loading text if wallet address just needs to be entered
+                        if (!state.isWalletLoading) {
+                            Text(stringResource(R.string.dashboard_chart_set_wallet))
+                        }
+                     }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun HashRateChart(
+    tenMinData: List<ChartDataPoint>,
+    twoHourData: List<ChartDataPoint>,
+    modifier: Modifier = Modifier
+) {
+    // Define colors from theme or directly
+    val surfaceColorHex = "#${MaterialTheme.colorScheme.surface.toArgb().toUInt().toString(16).substring(2)}" // Get hex like #1F2C40
+    val onSurfaceVariantColorHex = "#${MaterialTheme.colorScheme.onSurfaceVariant.toArgb().toUInt().toString(16).substring(2)}" // Get hex like #A2A6AC
+    val line10MinColorHex = "#6cbcd0" // Light Blue
+    val line2HourColorHex = "#d5a326" // Yellow/Gold
+
+    // Resolve strings outside AndroidView
+    val series10MinName = stringResource(R.string.dashboard_chart_series_10min)
+    val series2HourName = stringResource(R.string.dashboard_chart_series_2hour)
+
+    // Use update lambda of AndroidView for configuration
+    AndroidView(
+        factory = { ctx -> AnyChartView(ctx) },
+        modifier = modifier,
+        update = { view ->
+            Log.d("HashRateChart", "Updating AnyChart styles and data")
+
+            // Prepare data (as before)
+            val seriesData10Min = tenMinData.map {
+                ValueDataEntry(it.timestamp.toInstant().toEpochMilli(), it.hashRate)
+            }
+            val seriesData2Hour = twoHourData.map {
+                ValueDataEntry(it.timestamp.toInstant().toEpochMilli(), it.hashRate)
+            }
+
+            val cartesian: Cartesian = AnyChart.line()
+            APIlib.getInstance().setActiveAnyChartView(view)
+
+            // --- Styling --- >
+            cartesian.background().fill(surfaceColorHex) // Set background color
+            cartesian.animation(true)
+            cartesian.padding(10.0, 20.0, 5.0, 5.0)
+
+            cartesian.crosshair().enabled(true)
+            cartesian.crosshair()
+                .yLabel(true)
+                .yStroke(null as Stroke?, null, null, null as String?, null as String?)
+
+            cartesian.tooltip().positionMode(TooltipPositionMode.POINT)
+
+            // Axis Styling
+            cartesian.yAxis(0).title(false)
+            cartesian.xAxis(0).labels().padding(5.0, 5.0, 5.0, 5.0)
+            cartesian.xAxis(0).labels().format("{%Value}{dateTimeFormat:HH:mm}")
+            cartesian.xAxis(0).labels().fontColor(onSurfaceVariantColorHex) // Set X-axis label color
+            cartesian.yAxis(0).labels().format("{%Value}{scale:(1000)(1000)(1000)(1000)|( H/s)( KH/s)( MH/s)( GH/s)( TH/s)}")
+            cartesian.yAxis(0).labels().fontColor(onSurfaceVariantColorHex) // Set Y-axis label color
+
+            // Grid lines (optional, set color if desired)
+            // cartesian.yGrid(0).stroke("#ffffff 0.1")
+            // cartesian.xGrid(0).stroke("#ffffff 0.1")
+
+            // --- Series Configuration --- >
+            // Series 1: 10 Minute (Blue)
+            val series10Min = cartesian.line(seriesData10Min)
+            series10Min.name(series10MinName)
+            series10Min.color(line10MinColorHex) // Use defined blue color
+            series10Min.hovered().markers().enabled(true)
+            series10Min.hovered().markers().type(MarkerType.CIRCLE).size(4.0)
+            series10Min.tooltip()
+                .position("right")
+                .anchor(Anchor.LEFT_CENTER)
+                .offsetX(5.0)
+                .offsetY(5.0)
+                .format("10m - {%x}{dateTimeFormat:dd MMM HH:mm}: {%Value}{scale:(1000)(1000)(1000)(1000)|( H/s)( KH/s)( MH/s)( GH/s)( TH/s)}")
+
+            // Series 2: 2 Hour (Yellow/Gold)
+            if (seriesData2Hour.isNotEmpty()) {
+                val series2Hour = cartesian.line(seriesData2Hour)
+                series2Hour.name(series2HourName)
+                series2Hour.color(line2HourColorHex) // Use defined yellow color
+                series2Hour.hovered().markers().enabled(true)
+                series2Hour.hovered().markers().type(MarkerType.CIRCLE).size(4.0)
+                series2Hour.tooltip()
+                    .position("right")
+                    .anchor(Anchor.LEFT_CENTER)
+                    .offsetX(5.0)
+                    .offsetY(5.0)
+                    .format("2h - {%x}{dateTimeFormat:dd MMM HH:mm}: {%Value}{scale:(1000)(1000)(1000)(1000)|( H/s)( KH/s)( MH/s)( GH/s)( TH/s)}")
+            }
+
+            // --- Final Chart Setup --- >
+            cartesian.xScale(com.anychart.scales.DateTime.instantiate())
+            cartesian.legend().enabled(true)
+            cartesian.legend().fontColor(onSurfaceVariantColorHex) // Set legend text color
+            cartesian.legend().fontSize(13.0)
+            cartesian.legend().padding(0.0, 0.0, 10.0, 0.0)
+
+            view.setChart(cartesian)
+        }
+    )
+}

+ 51 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardMvi.kt

@@ -0,0 +1,51 @@
+package com.codeskraps.publicpool.presentation.dashboard
+
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+import com.codeskraps.publicpool.domain.model.ClientInfo
+import com.codeskraps.publicpool.domain.model.NetworkInfo
+import com.codeskraps.publicpool.presentation.common.UiEffect
+import com.codeskraps.publicpool.presentation.common.UiEvent
+import com.codeskraps.publicpool.presentation.common.UiState
+
+// --- State ---
+data class DashboardState(
+    // Wallet Address
+    val walletAddress: String? = null,
+
+    // Data States
+    val networkInfo: NetworkInfo? = null,
+    val clientInfo: ClientInfo? = null,
+    val chartData: List<ChartDataPoint> = emptyList(),
+    val chartDataTwoHourAvg: List<ChartDataPoint> = emptyList(),
+
+    // Loading States
+    val isWalletLoading: Boolean = true, // Loading wallet from DataStore
+    val isNetworkLoading: Boolean = false,
+    val isClientInfoLoading: Boolean = false,
+    val isChartDataLoading: Boolean = false,
+
+    // Error States (can be more granular if needed)
+    val errorMessage: String? = null
+
+) : UiState {
+    // Combined loading state for overall screen indication (optional)
+    val isLoading: Boolean
+        get() = isWalletLoading || isNetworkLoading || isClientInfoLoading || isChartDataLoading
+}
+
+// --- Events ---
+sealed interface DashboardEvent : UiEvent {
+    data object LoadData : DashboardEvent // Initial load trigger
+    data object RefreshData : DashboardEvent
+    data object GoToSettings : DashboardEvent
+    data class WalletAddressLoaded(val address: String?) : DashboardEvent // Internal event
+    data class NetworkInfoResult(val result: Result<NetworkInfo>) : DashboardEvent // Internal event
+    data class ClientInfoResult(val result: Result<ClientInfo>) : DashboardEvent // Internal event
+    data class ChartDataResult(val result: Result<List<ChartDataPoint>>) : DashboardEvent // Internal event
+}
+
+// --- Effects ---
+sealed interface DashboardEffect : UiEffect {
+    data object NavigateToSettings : DashboardEffect
+    data class ShowErrorSnackbar(val message: String) : DashboardEffect
+} 

+ 177 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/dashboard/DashboardScreenModel.kt

@@ -0,0 +1,177 @@
+package com.codeskraps.publicpool.presentation.dashboard
+
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.screenModelScope
+import com.codeskraps.publicpool.domain.model.ChartDataPoint
+import com.codeskraps.publicpool.domain.usecase.CalculateTwoHourAverageUseCase
+import com.codeskraps.publicpool.domain.usecase.GetChartDataUseCase
+import com.codeskraps.publicpool.domain.usecase.GetClientInfoUseCase
+import com.codeskraps.publicpool.domain.usecase.GetNetworkInfoUseCase
+import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.di.AppReadinessState
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+
+class DashboardScreenModel(
+    private val getWalletAddressUseCase: GetWalletAddressUseCase,
+    private val getNetworkInfoUseCase: GetNetworkInfoUseCase,
+    private val getClientInfoUseCase: GetClientInfoUseCase,
+    private val getChartDataUseCase: GetChartDataUseCase,
+    private val calculateTwoHourAverageUseCase: CalculateTwoHourAverageUseCase,
+    private val appReadinessState: AppReadinessState
+) : StateScreenModel<DashboardState>(DashboardState()) {
+
+    private val _effect = Channel<DashboardEffect>()
+    val effect = _effect.receiveAsFlow()
+
+    private var dataLoadingJob: Job? = null
+
+    init {
+        // Start loading data immediately
+        handleEvent(DashboardEvent.LoadData)
+    }
+
+    fun handleEvent(event: DashboardEvent) {
+        when (event) {
+            DashboardEvent.LoadData -> loadInitialData()
+            DashboardEvent.RefreshData -> refreshData()
+            DashboardEvent.GoToSettings -> sendEffect(DashboardEffect.NavigateToSettings)
+            // Internal Events triggered by data loading flows/calls
+            is DashboardEvent.WalletAddressLoaded -> processWalletAddress(event.address)
+            is DashboardEvent.NetworkInfoResult -> processNetworkInfoResult(event.result)
+            is DashboardEvent.ClientInfoResult -> processClientInfoResult(event.result)
+            is DashboardEvent.ChartDataResult -> processChartDataResult(event.result)
+        }
+    }
+
+    private fun loadInitialData() {
+        // Collect wallet address changes
+        screenModelScope.launch {
+            getWalletAddressUseCase()
+                .onStart { mutableState.update { it.copy(isWalletLoading = true) } }
+                .catch { e ->
+                    mutableState.update { it.copy(isWalletLoading = false, errorMessage = "Failed to load wallet address") }
+                    sendEffect(DashboardEffect.ShowErrorSnackbar("Error loading wallet: ${e.message}"))
+                }
+                .collect { address ->
+                    handleEvent(DashboardEvent.WalletAddressLoaded(address))
+                }
+        }
+
+        // Fetch Network Info (doesn't depend on wallet)
+        fetchNetworkInfo()
+    }
+
+    private fun processWalletAddress(address: String?) {
+        appReadinessState.setReady()
+
+        mutableState.update { it.copy(walletAddress = address, isWalletLoading = false) }
+        if (address != null && address.isNotBlank()) {
+            // Wallet address available, fetch client-specific data
+            fetchClientInfoAndChartData(address)
+        } else {
+            // No wallet address, clear client data and show appropriate message/state
+            mutableState.update {
+                it.copy(
+                    clientInfo = null,
+                    chartData = emptyList(),
+                    chartDataTwoHourAvg = emptyList(),
+                    isClientInfoLoading = false,
+                    isChartDataLoading = false,
+                    errorMessage = if (!it.isWalletLoading) "Please set a wallet address in Settings" else it.errorMessage
+                )
+            }
+            // Cancel any ongoing client/chart data fetching if wallet becomes null/blank
+            dataLoadingJob?.cancel()
+            dataLoadingJob = null
+        }
+    }
+
+    private fun refreshData() {
+        dataLoadingJob?.cancel() // Cancel previous jobs if any
+        mutableState.update { it.copy(errorMessage = null) } // Clear previous errors
+        fetchNetworkInfo()
+        state.value.walletAddress?.let { address ->
+            if (address.isNotBlank()) {
+                fetchClientInfoAndChartData(address, isRefresh = true)
+            }
+        }
+    }
+
+    private fun fetchNetworkInfo() {
+        screenModelScope.launch {
+            mutableState.update { it.copy(isNetworkLoading = true) }
+            val result = getNetworkInfoUseCase()
+            handleEvent(DashboardEvent.NetworkInfoResult(result))
+        }
+    }
+
+    private fun fetchClientInfoAndChartData(address: String, isRefresh: Boolean = false) {
+        dataLoadingJob?.cancel() // Cancel previous loads before starting new ones
+        dataLoadingJob = screenModelScope.launch {
+            mutableState.update {
+                it.copy(
+                    isClientInfoLoading = true,
+                    isChartDataLoading = true,
+                    // Consider clearing previous data on refresh if desired
+                    // clientInfo = if (isRefresh) it.clientInfo else null,
+                    // chartData = if (isRefresh) it.chartData else emptyList(),
+                    // chartDataTwoHourAvg = if (isRefresh) it.chartDataTwoHourAvg else emptyList()
+                )
+            }
+
+            // Launch both fetches concurrently
+            launch {
+                val clientInfoResult = getClientInfoUseCase(address)
+                handleEvent(DashboardEvent.ClientInfoResult(clientInfoResult))
+            }
+            launch {
+                val chartDataResult = getChartDataUseCase(address)
+                handleEvent(DashboardEvent.ChartDataResult(chartDataResult))
+            }
+        }
+    }
+
+    private fun processNetworkInfoResult(result: Result<com.codeskraps.publicpool.domain.model.NetworkInfo>) {
+        result.onSuccess {
+            mutableState.update { s -> s.copy(networkInfo = it, isNetworkLoading = false) }
+        }.onFailure {
+            mutableState.update { s -> s.copy(isNetworkLoading = false, errorMessage = "Failed to load network info") }
+            sendEffect(DashboardEffect.ShowErrorSnackbar("Network Error: ${it.message}"))
+        }
+    }
+
+    private fun processClientInfoResult(result: Result<com.codeskraps.publicpool.domain.model.ClientInfo>) {
+        result.onSuccess {
+            mutableState.update { s -> s.copy(clientInfo = it, isClientInfoLoading = false) }
+        }.onFailure {
+            mutableState.update { s -> s.copy(isClientInfoLoading = false, errorMessage = "Failed to load client info") }
+            sendEffect(DashboardEffect.ShowErrorSnackbar("Client Info Error: ${it.message}"))
+        }
+    }
+
+    private fun processChartDataResult(result: Result<List<ChartDataPoint>>) {
+        result.onSuccess {
+            // Calculate 2-hour average from the fetched 10-min data
+            val twoHourAvg = calculateTwoHourAverageUseCase(it)
+            mutableState.update { s ->
+                s.copy(
+                    chartData = it,
+                    chartDataTwoHourAvg = twoHourAvg,
+                    isChartDataLoading = false
+                )
+            }
+        }.onFailure {
+            mutableState.update { s -> s.copy(isChartDataLoading = false, errorMessage = "Failed to load chart data") }
+            sendEffect(DashboardEffect.ShowErrorSnackbar("Chart Data Error: ${it.message}"))
+        }
+    }
+
+    private fun sendEffect(effectToSend: DashboardEffect) {
+        screenModelScope.launch {
+            _effect.send(effectToSend)
+        }
+    }
+} 

+ 62 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/navigation/BottomTabs.kt

@@ -0,0 +1,62 @@
+package com.codeskraps.publicpool.presentation.navigation
+
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import cafe.adriel.voyager.navigator.tab.Tab
+import cafe.adriel.voyager.navigator.tab.TabOptions
+import com.codeskraps.publicpool.R
+import com.codeskraps.publicpool.presentation.wallet.WalletScreen
+import com.codeskraps.publicpool.presentation.workers.WorkersScreen
+
+internal data object DashboardTab : Tab {
+    private fun readResolve(): Any = DashboardTab
+    override val options: TabOptions
+        @Composable
+        get() {
+            val title = stringResource(id = R.string.tab_title_dashboard)
+            val icon = rememberVectorPainter(Icons.Default.Home)
+            return remember { TabOptions(index = 0u, title = title, icon = icon) }
+        }
+
+    @Composable
+    override fun Content() {
+        DashboardScreen.Content()
+    }
+}
+
+internal data object WorkersTab : Tab {
+    private fun readResolve(): Any = WorkersTab
+    override val options: TabOptions
+        @Composable
+        get() {
+            val title = stringResource(id = R.string.tab_title_workers)
+            val icon = painterResource(id = R.drawable.device_hub)
+            return remember { TabOptions(index = 1u, title = title, icon = icon) }
+        }
+
+    @Composable
+    override fun Content() {
+        WorkersScreen.Content()
+    }
+}
+
+internal data object WalletTab : Tab {
+    private fun readResolve(): Any = WalletTab
+    override val options: TabOptions
+        @Composable
+        get() {
+            val title = stringResource(id = R.string.tab_title_wallet)
+            val icon = painterResource(id = R.drawable.wallet)
+            return remember { TabOptions(index = 2u, title = title, icon = icon) }
+        }
+
+    @Composable
+    override fun Content() {
+        WalletScreen.Content()
+    }
+} 

+ 47 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/navigation/Screens.kt

@@ -0,0 +1,47 @@
+package com.codeskraps.publicpool.presentation.navigation
+
+import android.os.Parcelable
+import androidx.compose.runtime.Composable
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.core.screen.ScreenKey
+import cafe.adriel.voyager.core.screen.uniqueScreenKey
+import cafe.adriel.voyager.koin.koinScreenModel // Import the new function
+import com.codeskraps.publicpool.presentation.dashboard.DashboardScreenModel
+import com.codeskraps.publicpool.presentation.dashboard.DashboardContent // We'll create this Composable
+import com.codeskraps.publicpool.presentation.settings.SettingsScreenModel
+import com.codeskraps.publicpool.presentation.settings.SettingsContent // We'll create this Composable
+import kotlinx.parcelize.IgnoredOnParcel
+import kotlinx.parcelize.Parcelize // Required for Screen serialization if needed
+
+// Using Parcelize allows Screens to be potentially passed in bundles, though not strictly necessary
+// for basic navigation if you reconstruct them.
+
+@Parcelize
+data object DashboardScreen : Screen, Parcelable {
+    // Optional: Define a unique key if needed for specific navigator operations
+    @IgnoredOnParcel
+    override val key: ScreenKey = uniqueScreenKey
+
+    private fun readResolve(): Any = DashboardScreen
+
+    @Composable
+    override fun Content() {
+        // Use koinScreenModel from voyager-koin
+        val screenModel: DashboardScreenModel = koinScreenModel()
+        DashboardContent(screenModel) // Pass ScreenModel to the actual UI content
+    }
+}
+
+@Parcelize
+data object SettingsScreen : Screen, Parcelable {
+    @IgnoredOnParcel
+    override val key: ScreenKey = uniqueScreenKey
+
+    private fun readResolve(): Any = SettingsScreen
+
+    @Composable
+    override fun Content() {
+        val screenModel: SettingsScreenModel = koinScreenModel()
+        SettingsContent(screenModel)
+    }
+} 

+ 88 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsContent.kt

@@ -0,0 +1,88 @@
+package com.codeskraps.publicpool.presentation.settings
+
+import android.widget.Toast
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack // Use AutoMirrored for LTR/RTL
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.navigator.LocalNavigator
+import cafe.adriel.voyager.navigator.currentOrThrow
+import com.codeskraps.publicpool.R
+import com.codeskraps.publicpool.presentation.common.AppCard // Import AppCard
+import kotlinx.coroutines.flow.collectLatest
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun SettingsContent(screenModel: SettingsScreenModel) {
+    val state by screenModel.state.collectAsState() // Collect state from ScreenModel
+    val context = LocalContext.current
+    val navigator = LocalNavigator.currentOrThrow // Get the navigator
+
+    // Resolve strings needed inside LaunchedEffect here
+    val walletSavedMessage = stringResource(R.string.settings_toast_wallet_saved)
+
+    // Effect handling (e.g., showing toasts)
+    LaunchedEffect(key1 = screenModel.effect) {
+        screenModel.effect.collectLatest { effect ->
+            when (effect) {
+                is SettingsEffect.ShowError -> {
+                    Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+                }
+                SettingsEffect.WalletAddressSaved -> {
+                    // Use the pre-resolved string
+                    Toast.makeText(context, walletSavedMessage, Toast.LENGTH_SHORT).show()
+                }
+            }
+        }
+    }
+
+    Scaffold(
+        topBar = {
+            TopAppBar(
+                title = { Text(stringResource(R.string.screen_title_settings)) },
+                navigationIcon = {
+                    IconButton(onClick = { navigator.pop() }) {
+                        Icon(
+                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+                            contentDescription = stringResource(R.string.action_back)
+                        )
+                    }
+                }
+            )
+        }
+    ) { paddingValues ->
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(paddingValues)
+                .padding(16.dp),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.spacedBy(16.dp)
+        ) {
+            if (state.isLoading) {
+                CircularProgressIndicator()
+            } else {
+                OutlinedTextField(
+                    value = state.walletAddress,
+                    onValueChange = { screenModel.handleEvent(SettingsEvent.WalletAddressChanged(it)) },
+                    label = { Text(stringResource(R.string.settings_label_wallet_address)) },
+                    modifier = Modifier.fillMaxWidth(),
+                    singleLine = true
+                )
+
+                Button(
+                    onClick = { screenModel.handleEvent(SettingsEvent.SaveWalletAddress) },
+                    modifier = Modifier.align(Alignment.End)
+                ) {
+                    Text(stringResource(R.string.settings_button_save))
+                }
+            }
+        }
+    }
+} 

+ 24 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsMvi.kt

@@ -0,0 +1,24 @@
+package com.codeskraps.publicpool.presentation.settings
+
+import com.codeskraps.publicpool.presentation.common.UiEffect
+import com.codeskraps.publicpool.presentation.common.UiEvent
+import com.codeskraps.publicpool.presentation.common.UiState
+
+// --- State ---
+data class SettingsState(
+    val walletAddress: String = "",
+    val isLoading: Boolean = true // Start loading initially
+) : UiState
+
+// --- Events ---
+sealed interface SettingsEvent : UiEvent {
+    data class WalletAddressChanged(val address: String) : SettingsEvent
+    data object SaveWalletAddress : SettingsEvent
+    data object LoadWalletAddress : SettingsEvent // To trigger initial load
+}
+
+// --- Effects ---
+sealed interface SettingsEffect : UiEffect {
+    data object WalletAddressSaved : SettingsEffect
+    data class ShowError(val message: String) : SettingsEffect
+} 

+ 67 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/settings/SettingsScreenModel.kt

@@ -0,0 +1,67 @@
+package com.codeskraps.publicpool.presentation.settings
+
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.screenModelScope
+import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.SaveWalletAddressUseCase
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+
+class SettingsScreenModel(
+    private val getWalletAddressUseCase: GetWalletAddressUseCase,
+    private val saveWalletAddressUseCase: SaveWalletAddressUseCase
+) : StateScreenModel<SettingsState>(SettingsState()) { // Initialize with default state
+
+    private val _effect = Channel<SettingsEffect>()
+    val effect = _effect.receiveAsFlow()
+
+    init {
+        // Trigger loading the address when the ScreenModel is created
+        handleEvent(SettingsEvent.LoadWalletAddress)
+    }
+
+    fun handleEvent(event: SettingsEvent) {
+        when (event) {
+            is SettingsEvent.WalletAddressChanged -> {
+                // Update state directly for text field changes
+                mutableState.update { it.copy(walletAddress = event.address) }
+            }
+            SettingsEvent.SaveWalletAddress -> saveWalletAddress()
+            SettingsEvent.LoadWalletAddress -> loadWalletAddress()
+        }
+    }
+
+    private fun loadWalletAddress() {
+        screenModelScope.launch {
+            getWalletAddressUseCase()
+                .onStart { mutableState.update { it.copy(isLoading = true) } }
+                .catch { e ->
+                    // Handle error loading address
+                    mutableState.update { it.copy(isLoading = false) }
+                    _effect.send(SettingsEffect.ShowError("Failed to load wallet address: ${e.message}"))
+                }
+                .collect { address ->
+                    mutableState.update {
+                        it.copy(
+                            walletAddress = address ?: "",
+                            isLoading = false
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun saveWalletAddress() {
+        screenModelScope.launch {
+            try {
+                // Use the current address from the state
+                saveWalletAddressUseCase(mutableState.value.walletAddress)
+                _effect.send(SettingsEffect.WalletAddressSaved)
+            } catch (e: Exception) {
+                // Handle error saving address
+                _effect.send(SettingsEffect.ShowError("Failed to save wallet address: ${e.message}"))
+            }
+        }
+    }
+} 

+ 33 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletMvi.kt

@@ -0,0 +1,33 @@
+package com.codeskraps.publicpool.presentation.wallet
+
+import com.codeskraps.publicpool.domain.model.WalletInfo
+import com.codeskraps.publicpool.domain.model.CryptoPrice
+import com.codeskraps.publicpool.presentation.common.UiEffect
+import com.codeskraps.publicpool.presentation.common.UiEvent
+import com.codeskraps.publicpool.presentation.common.UiState
+
+// --- State ---
+data class WalletState(
+    val walletInfo: WalletInfo? = null,
+    val btcPrice: CryptoPrice? = null,
+    val isLoading: Boolean = false,
+    val isPriceLoading: Boolean = false,
+    val errorMessage: String? = null,
+    val walletAddress: String? = null, // To know which address to query
+    val isWalletLoading: Boolean = true // Loading address from DataStore
+) : UiState {
+    val isOverallLoading: Boolean
+        get() = isLoading || isWalletLoading || isPriceLoading
+}
+
+// --- Events ---
+sealed interface WalletEvent : UiEvent {
+    data object LoadWalletDetails : WalletEvent
+    data class WalletAddressLoaded(val address: String?) : WalletEvent // Internal
+    data class PriceResult(val result: Result<CryptoPrice>) : WalletEvent // Internal for price
+}
+
+// --- Effects ---
+sealed interface WalletEffect : UiEffect {
+    data class ShowError(val message: String) : WalletEffect
+} 

+ 321 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreen.kt

@@ -0,0 +1,321 @@
+package com.codeskraps.publicpool.presentation.wallet
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.os.Parcelable
+import android.widget.Toast
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.defaultMinSize
+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.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.koin.koinScreenModel
+import com.codeskraps.publicpool.R
+import com.codeskraps.publicpool.domain.model.CryptoPrice
+import com.codeskraps.publicpool.domain.model.WalletInfo
+import com.codeskraps.publicpool.domain.model.WalletTransaction
+import com.codeskraps.publicpool.presentation.common.AppCard
+import com.codeskraps.publicpool.ui.theme.PositiveGreen
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.parcelize.Parcelize
+import java.text.NumberFormat
+import java.time.format.DateTimeFormatter
+import java.util.Currency
+import java.util.Locale
+
+@Parcelize
+data object WalletScreen : Screen, Parcelable {
+    private fun readResolve(): Any = WalletScreen
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Composable
+    override fun Content() {
+        val screenModel: WalletScreenModel = koinScreenModel()
+        val state by screenModel.state.collectAsState()
+        val context = LocalContext.current
+
+        LaunchedEffect(key1 = screenModel.effect) {
+            screenModel.effect.collectLatest { effect ->
+                when (effect) {
+                    is WalletEffect.ShowError -> {
+                        Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+                    }
+                }
+            }
+        }
+
+        Scaffold(
+            topBar = { TopAppBar(title = { Text(stringResource(R.string.screen_title_wallet_details)) }) }
+        ) { paddingValues ->
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(paddingValues)
+            ) {
+                when {
+                    state.isWalletLoading || state.isLoading -> {
+                        // Combined loading indicator
+                        CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
+                    }
+                    state.walletAddress.isNullOrBlank() -> {
+                        // Use stringResource for message
+                        Text(
+                            text = stringResource(R.string.wallet_error_not_set),
+                            modifier = Modifier.align(Alignment.Center).padding(16.dp),
+                            textAlign = TextAlign.Center
+                        )
+                    }
+                    state.errorMessage != null -> {
+                        // Use stringResource for generic error, keep specific from state
+                        Text(
+                            text = state.errorMessage ?: stringResource(R.string.error_unknown),
+                            modifier = Modifier.align(Alignment.Center).padding(16.dp),
+                            color = MaterialTheme.colorScheme.error,
+                            textAlign = TextAlign.Center
+                        )
+                    }
+                    state.walletInfo == null -> {
+                         // Use stringResource for message
+                         Text(
+                            text = stringResource(R.string.wallet_error_load_failed),
+                            modifier = Modifier.align(Alignment.Center).padding(16.dp),
+                            textAlign = TextAlign.Center
+                        )
+                    }
+                    else -> {
+                        // Display Wallet Info and Transactions
+                        WalletDetailsContent(
+                            walletInfo = state.walletInfo!!,
+                            btcPrice = state.btcPrice
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun WalletDetailsContent(
+    walletInfo: WalletInfo,
+    btcPrice: CryptoPrice?
+) {
+    val context = LocalContext.current
+    val btcFormat = remember { "%.8f BTC" }
+    val dateFormatter = remember { DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm") }
+    val displayCurrency = stringResource(R.string.currency_usd)
+    val currencyFormat = remember {
+        NumberFormat.getCurrencyInstance(Locale.US).apply {
+            currency = Currency.getInstance(displayCurrency)
+            maximumFractionDigits = 2
+            minimumFractionDigits = 2
+        }
+    }
+
+    val finalBalanceFiat = remember(walletInfo.finalBalanceBtc, btcPrice) {
+        btcPrice?.let { walletInfo.finalBalanceBtc * it.price }
+    }
+
+    LazyColumn(
+        modifier = Modifier.fillMaxSize(),
+        contentPadding = PaddingValues(16.dp),
+        verticalArrangement = Arrangement.spacedBy(16.dp)
+    ) {
+        // Current Price Section
+        item {
+            CurrentPriceCard(btcPrice = btcPrice, currencyFormat = currencyFormat)
+        }
+
+        // Balance Section
+        item {
+            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+                BalanceCard(
+                    label = stringResource(R.string.wallet_label_final_balance),
+                    valueBtc = btcFormat.format(Locale.US, walletInfo.finalBalanceBtc),
+                    valueFiat = finalBalanceFiat?.let { currencyFormat.format(it) },
+                    fiatCurrencyLabel = btcPrice?.currency ?: stringResource(R.string.currency_usd),
+                    modifier = Modifier.weight(1f)
+                )
+            }
+        }
+
+        // Totals Section
+        item {
+            Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+                BalanceCard(
+                    label = stringResource(R.string.wallet_label_total_received),
+                    valueBtc = btcFormat.format(Locale.US, walletInfo.totalReceivedBtc),
+                    modifier = Modifier.weight(1f)
+                )
+                BalanceCard(
+                    label = stringResource(R.string.wallet_label_total_sent),
+                    valueBtc = btcFormat.format(Locale.US, walletInfo.totalSentBtc),
+                    modifier = Modifier.weight(1f)
+                )
+            }
+        }
+
+        // Transaction Header
+        item {
+            Text(
+                text = stringResource(R.string.wallet_header_recent_transactions_count, walletInfo.transactionCount),
+                style = MaterialTheme.typography.titleMedium,
+                modifier = Modifier.padding(top = 8.dp) // Add some space before tx list
+            )
+        }
+
+        // Transaction List
+        items(walletInfo.transactions, key = { it.hash }) { tx ->
+            TransactionItem(tx = tx, dateFormatter = dateFormatter)
+        }
+    }
+}
+
+@Composable
+fun CurrentPriceCard(btcPrice: CryptoPrice?, currencyFormat: NumberFormat) {
+    AppCard(modifier = Modifier.fillMaxWidth()) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .defaultMinSize(minHeight = 90.dp)
+                .padding(12.dp),
+            verticalArrangement = Arrangement.SpaceBetween
+        ) {
+            Text(
+                text = stringResource(R.string.wallet_label_current_btc_price),
+                style = MaterialTheme.typography.labelMedium,
+                color = MaterialTheme.colorScheme.onSurfaceVariant
+            )
+
+            Box(modifier = Modifier.align(Alignment.End)) {
+                if (btcPrice != null) {
+                    Text(
+                        text = currencyFormat.format(btcPrice.price),
+                        style = MaterialTheme.typography.headlineSmall,
+                        fontWeight = FontWeight.Bold,
+                        color = MaterialTheme.colorScheme.onSurface
+                    )
+                } else {
+                    Text(
+                        text = stringResource(R.string.text_placeholder_dash),
+                         style = MaterialTheme.typography.headlineSmall,
+                         fontWeight = FontWeight.Bold,
+                         color = MaterialTheme.colorScheme.onSurfaceVariant
+                     )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun BalanceCard(
+    label: String,
+    valueBtc: String,
+    valueFiat: String? = null,
+    fiatCurrencyLabel: String? = null,
+    modifier: Modifier = Modifier
+) {
+    AppCard(modifier = modifier) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .defaultMinSize(minHeight = 90.dp)
+                .padding(12.dp),
+            verticalArrangement = Arrangement.SpaceBetween
+        ) {
+            Text(
+                text = label,
+                style = MaterialTheme.typography.labelMedium,
+                color = MaterialTheme.colorScheme.onSurfaceVariant
+            )
+
+            Box(modifier = Modifier.align(Alignment.End)) {
+                Column(horizontalAlignment = Alignment.End) {
+                    Text(
+                        text = valueBtc,
+                        style = MaterialTheme.typography.bodyLarge,
+                        fontWeight = FontWeight.Bold,
+                        color = MaterialTheme.colorScheme.onSurface
+                    )
+                    Text(
+                        text = valueFiat?.let { "${stringResource(R.string.wallet_balance_fiat_prefix)} $it${fiatCurrencyLabel?.let { c -> " $c" } ?: ""}" } ?: "",
+                        style = MaterialTheme.typography.bodySmall,
+                        color = if (valueFiat != null) PositiveGreen else Color.Transparent
+                    )
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun TransactionItem(tx: WalletTransaction, dateFormatter: DateTimeFormatter) {
+    val btcFormat = remember { "%.8f BTC" }
+    val netValueFormatted = btcFormat.format(Locale.US, tx.resultBtc)
+    val timeFormatted = tx.time?.format(dateFormatter) ?: "Pending"
+    val valueColor = if (tx.resultSatoshis >= 0) Color(0xFF2E7D32) else MaterialTheme.colorScheme.error // Green for positive, Red for negative
+
+    AppCard(modifier = Modifier.fillMaxWidth()) {
+        Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
+            Row(
+                modifier = Modifier.fillMaxWidth(),
+                horizontalArrangement = Arrangement.SpaceBetween,
+                verticalAlignment = Alignment.CenterVertically
+            ) {
+                Text(text = timeFormatted, style = MaterialTheme.typography.labelMedium)
+                Text(text = netValueFormatted, style = MaterialTheme.typography.bodyMedium, color = valueColor)
+            }
+            Spacer(modifier = Modifier.height(4.dp))
+            Text(
+                text = tx.hash,
+                style = MaterialTheme.typography.bodySmall,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+                color = MaterialTheme.colorScheme.onSurfaceVariant
+            )
+        }
+    }
+}
+
+// Helper function to copy text to clipboard
+fun copyToClipboard(context: Context, text: String) {
+    val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+    // Use context.getString() for non-composable contexts
+    val label = context.getString(R.string.wallet_clipboard_label)
+    val message = context.getString(R.string.wallet_toast_address_copied)
+
+    val clip = ClipData.newPlainText(label, text)
+    clipboard.setPrimaryClip(clip)
+    Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
+} 

+ 105 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/wallet/WalletScreenModel.kt

@@ -0,0 +1,105 @@
+package com.codeskraps.publicpool.presentation.wallet
+
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.screenModelScope
+import com.codeskraps.publicpool.domain.usecase.GetBlockchainWalletInfoUseCase
+import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import com.codeskraps.publicpool.domain.usecase.GetBtcPriceUseCase
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+import android.util.Log
+
+class WalletScreenModel(
+    private val getWalletAddressUseCase: GetWalletAddressUseCase,
+    private val getBlockchainWalletInfoUseCase: GetBlockchainWalletInfoUseCase,
+    private val getBtcPriceUseCase: GetBtcPriceUseCase
+) : StateScreenModel<WalletState>(WalletState()) {
+
+    private val _effect = Channel<WalletEffect>()
+    val effect = _effect.receiveAsFlow()
+
+    init {
+        // Start loading wallet address immediately
+        handleEvent(WalletEvent.LoadWalletDetails)
+    }
+
+    fun handleEvent(event: WalletEvent) {
+        when (event) {
+            WalletEvent.LoadWalletDetails -> loadWalletAndDetails()
+            is WalletEvent.WalletAddressLoaded -> processWalletAddress(event.address)
+            is WalletEvent.PriceResult -> processPriceResult(event.result)
+        }
+    }
+
+    private fun loadWalletAndDetails() {
+        // Fetch BTC Price (doesn't depend on wallet address)
+        fetchBtcPrice()
+
+        // Fetch wallet address and details
+        screenModelScope.launch {
+            getWalletAddressUseCase()
+                .onStart { mutableState.update { it.copy(isWalletLoading = true) } }
+                .catch { e ->
+                    mutableState.update { it.copy(isWalletLoading = false, errorMessage = "Failed to load wallet address") }
+                    sendEffect(WalletEffect.ShowError("Error loading wallet: ${e.message}"))
+                }
+                .collect { address ->
+                    handleEvent(WalletEvent.WalletAddressLoaded(address))
+                }
+        }
+    }
+
+    private fun processWalletAddress(address: String?) {
+        mutableState.update { it.copy(walletAddress = address, isWalletLoading = false) }
+        if (address != null && address.isNotBlank()) {
+            fetchWalletDetails(address)
+        } else {
+            // No wallet address, clear info and show message
+            mutableState.update { it.copy(walletInfo = null, isLoading = false) }
+        }
+    }
+
+    private fun fetchWalletDetails(address: String) {
+        screenModelScope.launch {
+            mutableState.update { it.copy(isLoading = true, errorMessage = null) }
+            val result = getBlockchainWalletInfoUseCase(address)
+            result.onSuccess {
+                mutableState.update { s ->
+                    s.copy(
+                        walletInfo = it, // Store fetched WalletInfo
+                        isLoading = false
+                    )
+                }
+            }.onFailure {
+                mutableState.update { s ->
+                    s.copy(isLoading = false, errorMessage = "Failed to load wallet details")
+                }
+                sendEffect(WalletEffect.ShowError("Wallet details error: ${it.message}"))
+            }
+        }
+    }
+
+    private fun fetchBtcPrice() {
+        screenModelScope.launch {
+            mutableState.update { it.copy(isPriceLoading = true) }
+            val result = getBtcPriceUseCase()
+            handleEvent(WalletEvent.PriceResult(result))
+        }
+    }
+
+    private fun processPriceResult(result: Result<com.codeskraps.publicpool.domain.model.CryptoPrice>) {
+        result.onSuccess {
+            mutableState.update { s -> s.copy(btcPrice = it, isPriceLoading = false) }
+        }.onFailure {
+            mutableState.update { s -> s.copy(isPriceLoading = false) }
+            Log.e("WalletScreenModel", "Failed to load BTC price", it)
+        }
+    }
+
+    private fun sendEffect(effectToSend: WalletEffect) {
+        screenModelScope.launch {
+            _effect.send(effectToSend)
+        }
+    }
+} 

+ 28 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersMvi.kt

@@ -0,0 +1,28 @@
+package com.codeskraps.publicpool.presentation.workers
+
+import com.codeskraps.publicpool.domain.model.Worker
+import com.codeskraps.publicpool.presentation.common.UiEffect
+import com.codeskraps.publicpool.presentation.common.UiEvent
+import com.codeskraps.publicpool.presentation.common.UiState
+
+// --- State ---
+data class WorkersState(
+    // val workers: List<Worker> = emptyList(), // Removed redundant list
+    val isLoading: Boolean = false,
+    val errorMessage: String? = null,
+    val walletAddress: String? = null, // To know if we should fetch
+    val isWalletLoading: Boolean = true,
+    val groupedWorkers: Map<String, List<Worker>> = emptyMap() // Keep the grouped map used by UI
+) : UiState
+
+// --- Events ---
+sealed interface WorkersEvent : UiEvent {
+    data object LoadWorkers : WorkersEvent
+    data class WalletAddressLoaded(val address: String?) : WorkersEvent // Internal
+}
+
+// --- Effects ---
+sealed interface WorkersEffect : UiEffect {
+    // No specific effects needed for now, maybe error snackbars?
+    data class ShowError(val message: String) : WorkersEffect
+} 

+ 270 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreen.kt

@@ -0,0 +1,270 @@
+package com.codeskraps.publicpool.presentation.workers
+
+import android.os.Parcelable
+import android.widget.Toast
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+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
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.CircularProgressIndicator
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+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.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import cafe.adriel.voyager.core.screen.Screen
+import cafe.adriel.voyager.koin.koinScreenModel
+import com.codeskraps.publicpool.R
+import com.codeskraps.publicpool.domain.model.Worker
+import com.codeskraps.publicpool.util.calculateUptime
+import com.codeskraps.publicpool.util.formatDifficulty
+import com.codeskraps.publicpool.util.formatHashRate
+import com.codeskraps.publicpool.util.formatRelativeTime
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data object WorkersScreen : Screen, Parcelable {
+    private fun readResolve(): Any = WorkersScreen
+
+    @OptIn(ExperimentalMaterial3Api::class)
+    @Composable
+    override fun Content() {
+        val screenModel = koinScreenModel<WorkersScreenModel>()
+        val state by screenModel.state.collectAsState()
+        val context = LocalContext.current
+
+        LaunchedEffect(key1 = screenModel.effect) {
+            screenModel.effect.collectLatest { effect ->
+                when (effect) {
+                    is WorkersEffect.ShowError -> {
+                        Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
+                    }
+                }
+            }
+        }
+
+        Scaffold(
+            topBar = { TopAppBar(title = { Text(stringResource(id = R.string.screen_title_workers)) }) }
+        ) { paddingValues ->
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(paddingValues)
+            ) {
+                when {
+                    state.isLoading -> {
+                        Box(
+                            modifier = Modifier.fillMaxSize(),
+                            contentAlignment = Alignment.Center
+                        ) {
+                            CircularProgressIndicator()
+                        }
+                    }
+
+                    state.errorMessage != null -> {
+                        Text(
+                            text = state.errorMessage ?: stringResource(R.string.error_unknown),
+                            modifier = Modifier.align(Alignment.Center)
+                        )
+                    }
+
+                    state.groupedWorkers.isEmpty() && !state.isWalletLoading -> {
+                        Text(
+                            text = if (state.walletAddress.isNullOrBlank())
+                                stringResource(R.string.workers_no_wallet)
+                            else
+                                stringResource(R.string.workers_no_data),
+                            modifier = Modifier
+                                .align(Alignment.Center)
+                                .padding(16.dp),
+                            textAlign = TextAlign.Center
+                        )
+                    }
+
+                    else -> {
+                        WorkersList(groupedWorkers = state.groupedWorkers)
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun WorkersList(groupedWorkers: Map<String, List<Worker>>) {
+    LazyColumn(
+        modifier = Modifier.fillMaxSize(),
+        contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
+    ) {
+        items(groupedWorkers.entries.toList(), key = { it.key }) { (workerName, sessions) ->
+            WorkerGroupCard(workerName = workerName, sessions = sessions)
+            Spacer(modifier = Modifier.height(8.dp))
+        }
+    }
+}
+
+@Composable
+fun WorkerGroupCard(workerName: String, sessions: List<Worker>) {
+    var expanded by remember { mutableStateOf(false) }
+
+    val totalHashRate = sessions.sumOf { it.hashRate ?: 0.0 }
+    val groupBestDifficulty = sessions.mapNotNull { it.bestDifficulty }.maxOrNull()
+
+    Card(
+        modifier = Modifier.fillMaxWidth(),
+        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
+    ) {
+        Column(
+            modifier = Modifier
+                .animateContentSize()
+                .padding(horizontal = 16.dp, vertical = 8.dp)
+        ) {
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .clickable { expanded = !expanded },
+                verticalAlignment = Alignment.CenterVertically
+            ) {
+                Icon(
+                    painter = painterResource(id = if (expanded) R.drawable.expand_less else R.drawable.expand_more),
+                    contentDescription = if (expanded) stringResource(R.string.content_description_collapse)
+                    else stringResource(R.string.content_description_expand),
+                    modifier = Modifier.size(24.dp),
+                    tint = LocalContentColor.current
+                )
+                Spacer(modifier = Modifier.width(8.dp))
+                Text(
+                    workerName,
+                    style = MaterialTheme.typography.titleMedium,
+                    modifier = Modifier.weight(1f)
+                )
+                Spacer(modifier = Modifier.width(8.dp))
+                Column(horizontalAlignment = Alignment.End) {
+                    Text(
+                        stringResource(R.string.workers_label_session_count, sessions.size),
+                        style = MaterialTheme.typography.bodySmall
+                    )
+                    Text(formatHashRate(totalHashRate), style = MaterialTheme.typography.bodySmall)
+                    Text(
+                        formatDifficulty(groupBestDifficulty),
+                        style = MaterialTheme.typography.bodySmall
+                    )
+                }
+            }
+
+            if (expanded) {
+                HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
+                Row(modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(bottom = 4.dp)) {
+                    Text(
+                        stringResource(R.string.workers_header_session_id),
+                        modifier = Modifier.weight(1f),
+                        style = MaterialTheme.typography.labelSmall
+                    )
+                    Text(
+                        stringResource(R.string.workers_header_hash_rate),
+                        modifier = Modifier.weight(1f),
+                        style = MaterialTheme.typography.labelSmall,
+                        textAlign = TextAlign.End
+                    )
+                    Text(
+                        stringResource(R.string.workers_header_difficulty),
+                        modifier = Modifier.weight(1f),
+                        style = MaterialTheme.typography.labelSmall,
+                        textAlign = TextAlign.End
+                    )
+                    Text(
+                        stringResource(R.string.workers_header_uptime),
+                        modifier = Modifier.weight(1f),
+                        style = MaterialTheme.typography.labelSmall,
+                        textAlign = TextAlign.End
+                    )
+                    Text(
+                        stringResource(R.string.workers_header_last_seen),
+                        modifier = Modifier.weight(1f),
+                        style = MaterialTheme.typography.labelSmall,
+                        textAlign = TextAlign.End
+                    )
+                }
+                sessions.forEach { session ->
+                    WorkerSessionItem(session = session)
+                }
+            }
+        }
+    }
+}
+
+@Composable
+fun WorkerSessionItem(session: Worker) {
+    Row(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(vertical = 4.dp),
+        verticalAlignment = Alignment.CenterVertically
+    ) {
+        Text(
+            session.sessionId ?: stringResource(R.string.text_placeholder_dash),
+            modifier = Modifier.weight(1f),
+            style = MaterialTheme.typography.bodySmall,
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis
+        )
+        Text(
+            formatHashRate(session.hashRate ?: 0.0),
+            modifier = Modifier.weight(1f),
+            style = MaterialTheme.typography.bodySmall,
+            textAlign = TextAlign.End
+        )
+        Text(
+            formatDifficulty(session.bestDifficulty),
+            modifier = Modifier.weight(1f),
+            style = MaterialTheme.typography.bodySmall,
+            textAlign = TextAlign.End
+        )
+        Text(
+            calculateUptime(session.startTime),
+            modifier = Modifier.weight(1f),
+            style = MaterialTheme.typography.bodySmall,
+            textAlign = TextAlign.End
+        )
+        Text(
+            formatRelativeTime(session.lastSeen),
+            modifier = Modifier.weight(1f),
+            style = MaterialTheme.typography.bodySmall,
+            textAlign = TextAlign.End
+        )
+    }
+} 

+ 91 - 0
app/src/main/java/com/codeskraps/publicpool/presentation/workers/WorkersScreenModel.kt

@@ -0,0 +1,91 @@
+package com.codeskraps.publicpool.presentation.workers
+
+import cafe.adriel.voyager.core.model.StateScreenModel
+import cafe.adriel.voyager.core.model.screenModelScope
+import com.codeskraps.publicpool.presentation.workers.WorkersEffect
+import com.codeskraps.publicpool.presentation.workers.WorkersEvent
+import com.codeskraps.publicpool.presentation.workers.WorkersState
+import com.codeskraps.publicpool.domain.model.Worker
+import com.codeskraps.publicpool.domain.usecase.GetClientInfoUseCase
+import com.codeskraps.publicpool.domain.usecase.GetWalletAddressUseCase
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.launch
+
+class WorkersScreenModel(
+    private val getWalletAddressUseCase: GetWalletAddressUseCase,
+    private val getClientInfoUseCase: GetClientInfoUseCase
+) : StateScreenModel<WorkersState>(WorkersState()) {
+
+    private val _effect = Channel<WorkersEffect>()
+    val effect = _effect.receiveAsFlow()
+
+    init {
+        // Start loading wallet address immediately
+        handleEvent(WorkersEvent.LoadWorkers)
+    }
+
+    fun handleEvent(event: WorkersEvent) {
+        when (event) {
+            WorkersEvent.LoadWorkers -> loadWalletAndWorkers()
+            is WorkersEvent.WalletAddressLoaded -> processWalletAddress(event.address)
+        }
+    }
+
+    private fun loadWalletAndWorkers() {
+        screenModelScope.launch {
+            getWalletAddressUseCase()
+                .onStart { mutableState.update { it.copy(isWalletLoading = true) } }
+                .catch { e ->
+                    mutableState.update { it.copy(isWalletLoading = false, errorMessage = "Failed to load wallet address") }
+                    sendEffect(WorkersEffect.ShowError("Error loading wallet: ${e.message}"))
+                }
+                .collect { address ->
+                    handleEvent(WorkersEvent.WalletAddressLoaded(address))
+                }
+        }
+    }
+
+    private fun processWalletAddress(address: String?) {
+        mutableState.update { it.copy(walletAddress = address, isWalletLoading = false) }
+        if (!address.isNullOrBlank()) {
+            fetchWorkers(address)
+        } else {
+            // No wallet address, clear grouped workers and ensure loading is false
+            mutableState.update { it.copy(
+                groupedWorkers = emptyMap(), // Clear the grouped workers map
+                isLoading = false // Ensure isLoading is false
+                // Optionally reset error message: errorMessage = null
+                )
+            }
+        }
+    }
+
+    private fun fetchWorkers(address: String) {
+        screenModelScope.launch {
+            mutableState.update { it.copy(isLoading = true, errorMessage = null) }
+            val result = getClientInfoUseCase(address)
+            result.onSuccess { clientInfo -> // Get the full ClientInfo
+                // Group workers by name (Worker.id)
+                val grouped = clientInfo.workers.groupBy { it.id }
+                mutableState.update { s ->
+                    s.copy(
+                        groupedWorkers = grouped,
+                        isLoading = false
+                    )
+                }
+            }.onFailure {
+                mutableState.update { s ->
+                    s.copy(isLoading = false, errorMessage = "Failed to load workers")
+                }
+                sendEffect(WorkersEffect.ShowError("Worker loading error: ${it.message}"))
+            }
+        }
+    }
+
+    private fun sendEffect(effectToSend: WorkersEffect) {
+        screenModelScope.launch {
+            _effect.send(effectToSend)
+        }
+    }
+} 

+ 12 - 6
app/src/main/java/com/codeskraps/publicpool/ui/theme/Color.kt

@@ -2,10 +2,16 @@ package com.codeskraps.publicpool.ui.theme
 
 import androidx.compose.ui.graphics.Color
 
-val Purple80 = Color(0xFFD0BCFF)
-val PurpleGrey80 = Color(0xFFCCC2DC)
-val Pink80 = Color(0xFFEFB8C8)
+val AppBackground = Color(0xFF17212F)
+val AppSurface = Color(0xFF1F2C40)
+val AppOnSurface = Color(0xFFFFFFFF)
+val AppOnSurfaceVariant = Color(0xFFA2A6AC)
 
-val Purple40 = Color(0xFF6650a4)
-val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
+val DarkPrimary = Color(0xFF7B8DEF)
+val DarkOnPrimary = Color(0xFF000000)
+val DarkSecondary = Color(0xFFB0BEC5)
+val DarkOnSecondary = Color(0xFF000000)
+val DarkError = Color(0xFFCF6679)
+val DarkOnError = Color(0xFF000000)
+val CardBorder = Color(0xFF304562)
+val PositiveGreen = Color(0xFF4CAF50)

+ 44 - 33
app/src/main/java/com/codeskraps/publicpool/ui/theme/Theme.kt

@@ -2,52 +2,63 @@ package com.codeskraps.publicpool.ui.theme
 
 import android.app.Activity
 import android.os.Build
-import androidx.compose.foundation.isSystemInDarkTheme
+// import androidx.compose.foundation.isSystemInDarkTheme // No longer needed
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.darkColorScheme
-import androidx.compose.material3.dynamicDarkColorScheme
-import androidx.compose.material3.dynamicLightColorScheme
-import androidx.compose.material3.lightColorScheme
+// Remove dynamic/light color scheme imports if not used at all
+// import androidx.compose.material3.dynamicDarkColorScheme
+// import androidx.compose.material3.dynamicLightColorScheme
+// import androidx.compose.material3.lightColorScheme
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.WindowCompat
 
-private val DarkColorScheme = darkColorScheme(
-    primary = Purple80,
-    secondary = PurpleGrey80,
-    tertiary = Pink80
+// The defined dark color scheme using custom colors
+private val PublicPoolColorScheme = darkColorScheme(
+    primary = DarkPrimary,
+    onPrimary = DarkOnPrimary,
+    secondary = DarkSecondary,
+    onSecondary = DarkOnSecondary,
+    background = AppBackground,
+    surface = AppSurface, // Used by TopAppBar, Card, NavigationBar
+    surfaceContainer = AppSurface, // Explicitly set for components like Nav Bar
+    onBackground = AppOnSurface,
+    onSurface = AppOnSurface,
+    onSurfaceVariant = AppOnSurfaceVariant,
+    error = DarkError,
+    onError = DarkOnError,
+    outline = CardBorder // Use the border color for outlines
 )
 
-private val LightColorScheme = lightColorScheme(
-    primary = Purple40,
-    secondary = PurpleGrey40,
-    tertiary = Pink40
-
-    /* Other default colors to override
-    background = Color(0xFFFFFBFE),
-    surface = Color(0xFFFFFBFE),
-    onPrimary = Color.White,
-    onSecondary = Color.White,
-    onTertiary = Color.White,
-    onBackground = Color(0xFF1C1B1F),
-    onSurface = Color(0xFF1C1B1F),
-    */
-)
+// Remove LightColorScheme if not needed
+/*
+private val LightColorScheme = lightColorScheme(...)
+*/
 
 @Composable
 fun PublicPoolTheme(
-    darkTheme: Boolean = isSystemInDarkTheme(),
-    // Dynamic color is available on Android 12+
-    dynamicColor: Boolean = true,
+    // Remove darkTheme and dynamicColor parameters
     content: @Composable () -> Unit
 ) {
-    val colorScheme = when {
-        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
-            val context = LocalContext.current
-            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
-        }
+    // Always use the custom dark color scheme
+    val colorScheme = PublicPoolColorScheme
 
-        darkTheme -> DarkColorScheme
-        else -> LightColorScheme
+    val view = LocalView.current
+    if (!view.isInEditMode) {
+        SideEffect {
+            val window = (view.context as Activity).window
+            window.statusBarColor = colorScheme.surface.toArgb()
+            // Set navigation bar color if desired
+            // window.navigationBarColor = colorScheme.surface.toArgb()
+
+            // Always set status bar icons to light (because background is dark)
+            WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = false
+            // Always set navigation bar icons to light if nav bar color is dark
+            // WindowCompat.getInsetsController(window, view).isAppearanceLightNavigationBars = false
+        }
     }
 
     MaterialTheme(

+ 173 - 0
app/src/main/java/com/codeskraps/publicpool/util/Formatters.kt

@@ -0,0 +1,173 @@
+package com.codeskraps.publicpool.util
+
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.text.NumberFormat
+import java.util.* // Required for Locale
+import kotlin.math.pow
+import java.time.Duration
+import java.time.OffsetDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.DateTimeParseException
+
+// Suffixes for large numbers (SI prefixes)
+private val suffixes = charArrayOf(' ', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
+
+/**
+ * Formats a large double value into a more readable string with SI prefixes (k, M, G, T, etc.).
+ * Handles NaN, Infinity, and Zero gracefully.
+ * Uses appropriate precision.
+ */
+fun formatLargeNumber(number: Double, precision: Int = 2): String {
+    if (number.isNaN() || number.isInfinite() || number == 0.0) {
+        return "0"
+    }
+
+    // Use BigDecimal for accurate length calculation, especially for scientific notation
+    val numString = number.toBigDecimal().toPlainString()
+    val integerChars = numString.takeWhile { it != '.' }.length
+
+    // Determine the correct tier (0 for < 1000, 1 for k, 2 for M, etc.)
+    val tier = ((integerChars - 1).coerceAtLeast(0)) / 3
+
+    if (tier == 0) {
+        // For numbers less than 1000, format with commas and precision
+        val format = NumberFormat.getNumberInstance(Locale.US)
+        format.maximumFractionDigits = precision
+        format.minimumFractionDigits = 0 // Don't force trailing zeros if not needed
+        format.roundingMode = RoundingMode.FLOOR // Or RoundingMode.HALF_UP
+        return format.format(number)
+    }
+
+    // Ensure tier doesn't exceed available suffixes
+    val safeTier = tier.coerceAtMost(suffixes.size - 1)
+    val suffix = suffixes[safeTier]
+    val scale = 1000.0.pow(safeTier) // Use 1000 for SI prefixes
+    val scaled = number / scale
+
+    val format = NumberFormat.getNumberInstance(Locale.US)
+    format.maximumFractionDigits = precision
+    format.minimumFractionDigits = 0
+    format.roundingMode = RoundingMode.FLOOR // Or RoundingMode.HALF_UP
+    val formattedScaled = format.format(scaled)
+
+    // Add space only if suffix is not empty (i.e., not tier 0)
+    return "$formattedScaled${if (suffix != ' ') " $suffix" else ""}"
+}
+
+/**
+ * Formats a hash rate (in H/s) into a readable string with appropriate SI units (KH/s, MH/s, etc.).
+ * Handles NaN, Infinity, and Zero gracefully.
+ */
+fun formatHashRate(hashps: Double, precision: Int = 2): String {
+    if (hashps.isNaN() || hashps.isInfinite() || hashps == 0.0) {
+        return "0 H/s"
+    }
+
+    val units = listOf("H/s", "KH/s", "MH/s", "GH/s", "TH/s", "PH/s", "EH/s", "ZH/s", "YH/s")
+    var tier = 0
+    var scaledHashps = hashps
+
+    // Divide by 1000 until the number is manageable or we run out of units
+    while (scaledHashps >= 1000.0 && tier < units.size - 1) {
+        scaledHashps /= 1000.0
+        tier++
+    }
+
+    val format = NumberFormat.getNumberInstance(Locale.US)
+    format.maximumFractionDigits = precision
+    format.minimumFractionDigits = 0
+    format.roundingMode = RoundingMode.FLOOR // Or RoundingMode.HALF_UP
+    val formattedScaled = format.format(scaledHashps)
+
+    return "$formattedScaled ${units[tier]}"
+}
+
+/**
+ * Formats a BigDecimal value with specified precision, returning "0" if null.
+ */
+fun formatBigDecimal(value: BigDecimal?, precision: Int = 2): String {
+    if (value == null) return "0"
+    val format = NumberFormat.getNumberInstance(Locale.US)
+    format.maximumFractionDigits = precision
+    format.minimumFractionDigits = 0
+    format.roundingMode = RoundingMode.FLOOR
+    return format.format(value)
+}
+
+// --- Added from ui/utils ---
+
+fun formatDifficulty(difficulty: Double?): String {
+    if (difficulty == null || difficulty == 0.0) return "0.0"
+    // Using the logic similar to formatLargeNumber but specific for difficulty formatting
+    // Assuming k, M, G, T are appropriate suffixes for difficulty
+    val numString = difficulty.toBigDecimal().toPlainString()
+    val integerChars = numString.takeWhile { it != '.' }.length
+    val tier = ((integerChars - 1).coerceAtLeast(0)) / 3
+
+    if (tier == 0) {
+        // Format with precision, no suffix
+        val format = NumberFormat.getNumberInstance(Locale.US)
+        format.maximumFractionDigits = 2 // Adjust precision as needed
+        format.minimumFractionDigits = 0
+        format.roundingMode = RoundingMode.FLOOR
+        return format.format(difficulty)
+    }
+
+    val safeTier = tier.coerceAtMost(suffixes.size - 1) // Use existing suffixes array
+    val suffix = suffixes[safeTier]
+    val scale = 1000.0.pow(safeTier)
+    val scaled = difficulty / scale
+
+    val format = NumberFormat.getNumberInstance(Locale.US)
+    format.maximumFractionDigits = 2 // Adjust precision as needed
+    format.minimumFractionDigits = 0
+    format.roundingMode = RoundingMode.FLOOR
+    val formattedScaled = format.format(scaled)
+
+    return "$formattedScaled${if (suffix != ' ') "$suffix" else ""}" // No space before suffix based on sample (4.37k)
+}
+
+// Basic relative time formatter - enhance with a library like ThreeTenABP or java.time if needed
+fun formatRelativeTime(isoDateTimeString: String?): String {
+    if (isoDateTimeString == null) return "N/A"
+    return try {
+        val dateTime = OffsetDateTime.parse(isoDateTimeString)
+        val now = OffsetDateTime.now(dateTime.offset) // Compare using the same offset
+        val duration = Duration.between(dateTime, now)
+
+        when {
+            duration.seconds < 60 -> "Just now"
+            duration.toMinutes() < 60 -> "${duration.toMinutes()} min ago"
+            duration.toHours() < 24 -> "${duration.toHours()}h ago"
+            duration.toDays() < 7 -> "${duration.toDays()}d ago"
+            else -> dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE) // Fallback to date
+        }
+    } catch (e: DateTimeParseException) {
+        "Invalid date" // Handle parsing error
+    }
+}
+
+fun calculateUptime(isoDateTimeString: String?): String {
+     if (isoDateTimeString == null) return "N/A"
+    return try {
+        val startTime = OffsetDateTime.parse(isoDateTimeString)
+        val now = OffsetDateTime.now(startTime.offset)
+        val duration = Duration.between(startTime, now)
+
+        val days = duration.toDays()
+        val hours = duration.toHours() % 24
+        val minutes = duration.toMinutes() % 60
+        // val seconds = duration.seconds % 60
+
+        buildString {
+            if (days > 0) append("${days}d ")
+            if (hours > 0 || days > 0) append("${hours}h ") // Show hours if days > 0
+            append("${minutes}m") // Always show minutes
+           // append("${seconds}s") // Optionally add seconds
+        }
+
+    } catch (e: DateTimeParseException) {
+        "Invalid date" // Handle parsing error
+    }
+} 

+ 5 - 0
app/src/main/res/drawable/device_hub.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M17,16l-4,-4V8.82C14.16,8.4 15,7.3 15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6c0,1.3 0.84,2.4 2,2.82V12l-4,4H3v5h5v-3.05l4,-4.2 4,4.2V21h5v-5h-4z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/drawable/expand_less.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
+    
+</vector>

+ 5 - 0
app/src/main/res/drawable/expand_more.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
+    
+</vector>

+ 0 - 170
app/src/main/res/drawable/ic_launcher_background.xml

@@ -1,170 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportWidth="108"
-    android:viewportHeight="108">
-    <path
-        android:fillColor="#3DDC84"
-        android:pathData="M0,0h108v108h-108z" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M9,0L9,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,0L19,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,0L29,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,0L39,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,0L49,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,0L59,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,0L69,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,0L79,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M89,0L89,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M99,0L99,108"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,9L108,9"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,19L108,19"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,29L108,29"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,39L108,39"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,49L108,49"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,59L108,59"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,69L108,69"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,79L108,79"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,89L108,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M0,99L108,99"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,29L89,29"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,39L89,39"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,49L89,49"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,59L89,59"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,69L89,69"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M19,79L89,79"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M29,19L29,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M39,19L39,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M49,19L49,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M59,19L59,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M69,19L69,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-    <path
-        android:fillColor="#00000000"
-        android:pathData="M79,19L79,89"
-        android:strokeWidth="0.8"
-        android:strokeColor="#33FFFFFF" />
-</vector>

Разница между файлами не показана из-за своего большого размера
+ 3 - 27
app/src/main/res/drawable/ic_launcher_foreground.xml


+ 5 - 0
app/src/main/res/drawable/wallet.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M18,4H6C3.79,4 2,5.79 2,8v8c0,2.21 1.79,4 4,4h12c2.21,0 4,-1.79 4,-4V8C22,5.79 20.21,4 18,4zM16.14,13.77c-0.24,0.2 -0.57,0.28 -0.88,0.2L4.15,11.25C4.45,10.52 5.16,10 6,10h12c0.67,0 1.26,0.34 1.63,0.84L16.14,13.77zM6,6h12c1.1,0 2,0.9 2,2v0.55C19.41,8.21 18.73,8 18,8H6C5.27,8 4.59,8.21 4,8.55V8C4,6.9 4.9,6 6,6z"/>
+    
+</vector>

+ 2 - 3
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@drawable/ic_launcher_background" />
-    <foreground android:drawable="@drawable/ic_launcher_foreground" />
-    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
 </adaptive-icon>

+ 2 - 3
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@drawable/ic_launcher_background" />
-    <foreground android:drawable="@drawable/ic_launcher_foreground" />
-    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+    <background android:drawable="@color/ic_launcher_background"/>
+    <foreground android:drawable="@drawable/ic_launcher_foreground"/>
 </adaptive-icon>

BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 61 - 0
app/src/main/res/values-de/strings.xml

@@ -0,0 +1,61 @@
+<resources>
+    <string name="app_name">PublicPool</string>
+
+    <!-- Tab Titles -->
+    <string name="tab_title_dashboard">Dashboard</string>
+    <string name="tab_title_workers">Arbeiter</string>
+    <string name="tab_title_wallet">Wallet</string>
+
+    <!-- General -->
+    <string name="error_unknown">Unbekannter Fehler</string>
+    <string name="text_placeholder_dash">-</string>
+    <string name="currency_usd">USD</string>
+    <string name="action_back">Zurück</string>
+
+    <!-- Settings Screen -->
+    <string name="screen_title_settings">Einstellungen</string>
+    <string name="settings_label_wallet_address">Bitcoin-Wallet-Adresse</string>
+    <string name="settings_button_save">Speichern</string>
+    <string name="settings_toast_wallet_saved">Wallet-Adresse gespeichert</string>
+
+    <!-- Wallet Screen -->
+    <string name="screen_title_wallet_details">Wallet-Details</string>
+    <string name="wallet_error_not_set">Wallet-Adresse nicht festgelegt. Bitte konfigurieren Sie sie in den Einstellungen.</string>
+    <string name="wallet_error_load_failed">Wallet-Informationen konnten nicht geladen werden.</string>
+    <string name="wallet_label_current_btc_price">Aktueller BTC-Preis</string>
+    <string name="wallet_label_final_balance">Endsaldo</string>
+    <string name="wallet_label_total_received">Gesamt empfangen</string>
+    <string name="wallet_label_total_sent">Gesamt gesendet</string>
+    <string name="wallet_header_recent_transactions_count">Letzte Transaktionen (%d)</string>
+    <string name="wallet_clipboard_label">Wallet-Adresse</string>
+    <string name="wallet_toast_address_copied">Adresse in die Zwischenablage kopiert</string>
+    <string name="wallet_balance_fiat_prefix">≈</string>
+
+    <!-- Workers Screen -->
+    <string name="screen_title_workers">Arbeiter</string>
+    <string name="workers_no_wallet">Bitte legen Sie Ihre Wallet-Adresse in den Einstellungen fest, um Arbeiterdetails anzuzeigen.</string>
+    <string name="workers_no_data">Keine aktiven Arbeiter für diese Wallet-Adresse gefunden.</string>
+    <string name="content_description_expand">Arbeiterdetails erweitern</string>
+    <string name="content_description_collapse">Arbeiterdetails reduzieren</string>
+    <string name="workers_header_session_id">Sitzungs-ID</string>
+    <string name="workers_header_hash_rate">Hash-Rate</string>
+    <string name="workers_header_difficulty">Schwierigkeit</string>
+    <string name="workers_header_uptime">Betriebszeit</string>
+    <string name="workers_header_last_seen">Zuletzt gesehen</string>
+    <string name="workers_label_session_count">%d Sitzung(en)</string>
+
+    <!-- Dashboard Screen -->
+    <string name="screen_title_dashboard">Public Pool</string>
+    <string name="dashboard_action_settings">Einstellungen</string>
+    <string name="dashboard_info_set_wallet">Bitte legen Sie Ihre Bitcoin-Wallet-Adresse in den Einstellungen fest, um Ihre Statistiken anzuzeigen.</string>
+    <string name="dashboard_card_label_your_best_difficulty">Ihre beste Schwierigkeit</string>
+    <string name="dashboard_card_label_network_difficulty">Netzwerk-Schwierigkeit</string>
+    <string name="dashboard_card_label_network_hash_rate">Netzwerk-Hash-Rate</string>
+    <string name="dashboard_card_label_block_height">Blockhöhe</string>
+    <string name="dashboard_card_secondary_block_weight_prefix">Gewicht:</string>
+    <string name="dashboard_chart_title">Hash-Raten-Verlauf</string>
+    <string name="dashboard_chart_no_data">Keine Diagrammdaten für diese Wallet verfügbar.</string>
+    <string name="dashboard_chart_set_wallet">Legen Sie die Wallet-Adresse fest, um das Diagramm anzuzeigen.</string>
+    <string name="dashboard_chart_series_10min">10 Minuten</string>
+    <string name="dashboard_chart_series_2hour">2 Stunden</string>
+</resources>

+ 61 - 0
app/src/main/res/values-es/strings.xml

@@ -0,0 +1,61 @@
+<resources>
+    <string name="app_name">PublicPool</string>
+
+    <!-- Tab Titles -->
+    <string name="tab_title_dashboard">Panel</string>
+    <string name="tab_title_workers">Trabajadores</string>
+    <string name="tab_title_wallet">Billetera</string>
+
+    <!-- General -->
+    <string name="error_unknown">Error desconocido</string>
+    <string name="text_placeholder_dash">-</string>
+    <string name="currency_usd">USD</string>
+    <string name="action_back">Atrás</string>
+
+    <!-- Settings Screen -->
+    <string name="screen_title_settings">Configuración</string>
+    <string name="settings_label_wallet_address">Dirección de billetera Bitcoin</string>
+    <string name="settings_button_save">Guardar</string>
+    <string name="settings_toast_wallet_saved">Dirección de billetera guardada</string>
+
+    <!-- Wallet Screen -->
+    <string name="screen_title_wallet_details">Detalles de la billetera</string>
+    <string name="wallet_error_not_set">Dirección de billetera no configurada. Por favor, configúrela en Ajustes.</string>
+    <string name="wallet_error_load_failed">No se pudo cargar la información de la billetera.</string>
+    <string name="wallet_label_current_btc_price">Precio actual de BTC</string>
+    <string name="wallet_label_final_balance">Saldo final</string>
+    <string name="wallet_label_total_received">Total recibido</string>
+    <string name="wallet_label_total_sent">Total enviado</string>
+    <string name="wallet_header_recent_transactions_count">Transacciones recientes (%d)</string>
+    <string name="wallet_clipboard_label">Dirección de billetera</string>
+    <string name="wallet_toast_address_copied">Dirección copiada al portapapeles</string>
+    <string name="wallet_balance_fiat_prefix">≈</string>
+
+    <!-- Workers Screen -->
+    <string name="screen_title_workers">Trabajadores</string>
+    <string name="workers_no_wallet">Por favor, configure su dirección de billetera en los ajustes para ver los detalles del trabajador.</string>
+    <string name="workers_no_data">No se encontraron trabajadores activos para esta dirección de billetera.</string>
+    <string name="content_description_expand">Expandir detalles del trabajador</string>
+    <string name="content_description_collapse">Contraer detalles del trabajador</string>
+    <string name="workers_header_session_id">ID de sesión</string>
+    <string name="workers_header_hash_rate">Tasa de hash</string>
+    <string name="workers_header_difficulty">Dificultad</string>
+    <string name="workers_header_uptime">Tiempo de actividad</string>
+    <string name="workers_header_last_seen">Visto por última vez</string>
+    <string name="workers_label_session_count">%d Sesión(es)</string>
+
+    <!-- Dashboard Screen -->
+    <string name="screen_title_dashboard">Public Pool</string>
+    <string name="dashboard_action_settings">Configuración</string>
+    <string name="dashboard_info_set_wallet">Por favor, configure su dirección de billetera Bitcoin en Configuración para ver sus estadísticas.</string>
+    <string name="dashboard_card_label_your_best_difficulty">Tu mejor dificultad</string>
+    <string name="dashboard_card_label_network_difficulty">Dificultad de la red</string>
+    <string name="dashboard_card_label_network_hash_rate">Tasa de hash de la red</string>
+    <string name="dashboard_card_label_block_height">Altura del bloque</string>
+    <string name="dashboard_card_secondary_block_weight_prefix">Peso:</string>
+    <string name="dashboard_chart_title">Historial de tasa de hash</string>
+    <string name="dashboard_chart_no_data">No hay datos de gráfico disponibles para esta billetera.</string>
+    <string name="dashboard_chart_set_wallet">Configure la dirección de la billetera para ver el gráfico.</string>
+    <string name="dashboard_chart_series_10min">10 minutos</string>
+    <string name="dashboard_chart_series_2hour">2 horas</string>
+</resources>

+ 61 - 0
app/src/main/res/values-fr/strings.xml

@@ -0,0 +1,61 @@
+<resources>
+    <string name="app_name">PublicPool</string>
+
+    <!-- Tab Titles -->
+    <string name="tab_title_dashboard">Tableau de bord</string>
+    <string name="tab_title_workers">Travailleurs</string>
+    <string name="tab_title_wallet">Portefeuille</string>
+
+    <!-- General -->
+    <string name="error_unknown">Erreur inconnue</string>
+    <string name="text_placeholder_dash">-</string>
+    <string name="currency_usd">USD</string>
+    <string name="action_back">Retour</string>
+
+    <!-- Settings Screen -->
+    <string name="screen_title_settings">Paramètres</string>
+    <string name="settings_label_wallet_address">Adresse du portefeuille Bitcoin</string>
+    <string name="settings_button_save">Enregistrer</string>
+    <string name="settings_toast_wallet_saved">Adresse du portefeuille enregistrée</string>
+
+    <!-- Wallet Screen -->
+    <string name="screen_title_wallet_details">Détails du portefeuille</string>
+    <string name="wallet_error_not_set">Adresse du portefeuille non définie. Veuillez la configurer dans les Paramètres.</string>
+    <string name="wallet_error_load_failed">Impossible de charger les informations du portefeuille.</string>
+    <string name="wallet_label_current_btc_price">Prix actuel du BTC</string>
+    <string name="wallet_label_final_balance">Solde final</string>
+    <string name="wallet_label_total_received">Total reçu</string>
+    <string name="wallet_label_total_sent">Total envoyé</string>
+    <string name="wallet_header_recent_transactions_count">Transactions récentes (%d)</string>
+    <string name="wallet_clipboard_label">Adresse du portefeuille</string>
+    <string name="wallet_toast_address_copied">Adresse copiée dans le presse-papiers</string>
+    <string name="wallet_balance_fiat_prefix">≈</string>
+
+    <!-- Workers Screen -->
+    <string name="screen_title_workers">Travailleurs</string>
+    <string name="workers_no_wallet">Veuillez définir votre adresse de portefeuille dans les paramètres pour afficher les détails du travailleur.</string>
+    <string name="workers_no_data">Aucun travailleur actif trouvé pour cette adresse de portefeuille.</string>
+    <string name="content_description_expand">Développer les détails du travailleur</string>
+    <string name="content_description_collapse">Réduire les détails du travailleur</string>
+    <string name="workers_header_session_id">ID de session</string>
+    <string name="workers_header_hash_rate">Taux de hachage</string>
+    <string name="workers_header_difficulty">Difficulté</string>
+    <string name="workers_header_uptime">Temps de disponibilité</string>
+    <string name="workers_header_last_seen">Vu pour la dernière fois</string>
+    <string name="workers_label_session_count">%d Session(s)</string>
+
+    <!-- Dashboard Screen -->
+    <string name="screen_title_dashboard">Public Pool</string>
+    <string name="dashboard_action_settings">Paramètres</string>
+    <string name="dashboard_info_set_wallet">Veuillez définir votre adresse de portefeuille Bitcoin dans les Paramètres pour voir vos statistiques.</string>
+    <string name="dashboard_card_label_your_best_difficulty">Votre meilleure difficulté</string>
+    <string name="dashboard_card_label_network_difficulty">Difficulté du réseau</string>
+    <string name="dashboard_card_label_network_hash_rate">Taux de hachage du réseau</string>
+    <string name="dashboard_card_label_block_height">Hauteur du bloc</string>
+    <string name="dashboard_card_secondary_block_weight_prefix">Poids:</string>
+    <string name="dashboard_chart_title">Historique du taux de hachage</string>
+    <string name="dashboard_chart_no_data">Aucune donnée de graphique disponible pour ce portefeuille.</string>
+    <string name="dashboard_chart_set_wallet">Définissez l'adresse du portefeuille pour voir le graphique.</string>
+    <string name="dashboard_chart_series_10min">10 minutes</string>
+    <string name="dashboard_chart_series_2hour">2 heures</string>
+</resources>

+ 61 - 0
app/src/main/res/values-hi/strings.xml

@@ -0,0 +1,61 @@
+<resources>
+    <string name="app_name">PublicPool</string>
+
+    <!-- Tab Titles -->
+    <string name="tab_title_dashboard">डैशबोर्ड</string>
+    <string name="tab_title_workers">वर्कर्स</string>
+    <string name="tab_title_wallet">वॉलेट</string>
+
+    <!-- General -->
+    <string name="error_unknown">अज्ञात त्रुटि</string>
+    <string name="text_placeholder_dash">-</string>
+    <string name="currency_usd">USD</string>
+    <string name="action_back">वापस</string>
+
+    <!-- Settings Screen -->
+    <string name="screen_title_settings">सेटिंग्स</string>
+    <string name="settings_label_wallet_address">बिटकॉइन वॉलेट पता</string>
+    <string name="settings_button_save">सहेजें</string>
+    <string name="settings_toast_wallet_saved">वॉलेट पता सहेजा गया</string>
+
+    <!-- Wallet Screen -->
+    <string name="screen_title_wallet_details">वॉलेट विवरण</string>
+    <string name="wallet_error_not_set">वॉलेट पता सेट नहीं है। कृपया सेटिंग्स में कॉन्फ़िगर करें।</string>
+    <string name="wallet_error_load_failed">वॉलेट जानकारी लोड नहीं हो सकी।</string>
+    <string name="wallet_label_current_btc_price">वर्तमान बीटीसी मूल्य</string>
+    <string name="wallet_label_final_balance">अंतिम शेष</string>
+    <string name="wallet_label_total_received">कुल प्राप्त</string>
+    <string name="wallet_label_total_sent">कुल भेजा गया</string>
+    <string name="wallet_header_recent_transactions_count">हाल के लेनदेन (%d)</string>
+    <string name="wallet_clipboard_label">वॉलेट पता</string>
+    <string name="wallet_toast_address_copied">पता क्लिपबोर्ड पर कॉपी किया गया</string>
+    <string name="wallet_balance_fiat_prefix">≈</string>
+
+    <!-- Workers Screen -->
+    <string name="screen_title_workers">वर्कर्स</string>
+    <string name="workers_no_wallet">वर्कर विवरण देखने के लिए कृपया सेटिंग्स में अपना वॉलेट पता सेट करें।</string>
+    <string name="workers_no_data">इस वॉलेट पते के लिए कोई सक्रिय वर्कर नहीं मिला।</string>
+    <string name="content_description_expand">वर्कर विवरण विस्तृत करें</string>
+    <string name="content_description_collapse">वर्कर विवरण संक्षिप्त करें</string>
+    <string name="workers_header_session_id">सत्र आईडी</string>
+    <string name="workers_header_hash_rate">हैश दर</string>
+    <string name="workers_header_difficulty">कठिनाई</string>
+    <string name="workers_header_uptime">अपटाइम</string>
+    <string name="workers_header_last_seen">अंतिम बार देखा गया</string>
+    <string name="workers_label_session_count">%d सत्र</string>
+
+    <!-- Dashboard Screen -->
+    <string name="screen_title_dashboard">पब्लिक पूल</string>
+    <string name="dashboard_action_settings">सेटिंग्स</string>
+    <string name="dashboard_info_set_wallet">अपने आँकड़े देखने के लिए कृपया सेटिंग्स में अपना बिटकॉइन वॉलेट पता सेट करें।</string>
+    <string name="dashboard_card_label_your_best_difficulty">आपकी सर्वश्रेष्ठ कठिनाई</string>
+    <string name="dashboard_card_label_network_difficulty">नेटवर्क कठिनाई</string>
+    <string name="dashboard_card_label_network_hash_rate">नेटवर्क हैश दर</string>
+    <string name="dashboard_card_label_block_height">ब्लॉक ऊंचाई</string>
+    <string name="dashboard_card_secondary_block_weight_prefix">वजन:</string>
+    <string name="dashboard_chart_title">हैश दर इतिहास</string>
+    <string name="dashboard_chart_no_data">इस वॉलेट के लिए कोई चार्ट डेटा उपलब्ध नहीं है।</string>
+    <string name="dashboard_chart_set_wallet">चार्ट देखने के लिए वॉलेट पता सेट करें।</string>
+    <string name="dashboard_chart_series_10min">10 मिनट</string>
+    <string name="dashboard_chart_series_2hour">2 घंटे</string>
+</resources>

+ 61 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -0,0 +1,61 @@
+<resources>
+    <string name="app_name">PublicPool</string>
+
+    <!-- Tab Titles -->
+    <string name="tab_title_dashboard">仪表盘</string>
+    <string name="tab_title_workers">矿工</string>
+    <string name="tab_title_wallet">钱包</string>
+
+    <!-- General -->
+    <string name="error_unknown">未知错误</string>
+    <string name="text_placeholder_dash">-</string>
+    <string name="currency_usd">美元</string>
+    <string name="action_back">返回</string>
+
+    <!-- Settings Screen -->
+    <string name="screen_title_settings">设置</string>
+    <string name="settings_label_wallet_address">比特币钱包地址</string>
+    <string name="settings_button_save">保存</string>
+    <string name="settings_toast_wallet_saved">钱包地址已保存</string>
+
+    <!-- Wallet Screen -->
+    <string name="screen_title_wallet_details">钱包详情</string>
+    <string name="wallet_error_not_set">未设置钱包地址。请在设置中配置。</string>
+    <string name="wallet_error_load_failed">无法加载钱包信息。</string>
+    <string name="wallet_label_current_btc_price">当前比特币价格</string>
+    <string name="wallet_label_final_balance">最终余额</string>
+    <string name="wallet_label_total_received">总接收</string>
+    <string name="wallet_label_total_sent">总发送</string>
+    <string name="wallet_header_recent_transactions_count">近期交易 (%d)</string>
+    <string name="wallet_clipboard_label">钱包地址</string>
+    <string name="wallet_toast_address_copied">地址已复制到剪贴板</string>
+    <string name="wallet_balance_fiat_prefix">≈</string>
+
+    <!-- Workers Screen -->
+    <string name="screen_title_workers">矿工</string>
+    <string name="workers_no_wallet">请在设置中设置您的钱包地址以查看矿工详细信息。</string>
+    <string name="workers_no_data">未找到此钱包地址的活动矿工。</string>
+    <string name="content_description_expand">展开矿工详情</string>
+    <string name="content_description_collapse">折叠矿工详情</string>
+    <string name="workers_header_session_id">会话 ID</string>
+    <string name="workers_header_hash_rate">哈希率</string>
+    <string name="workers_header_difficulty">难度</string>
+    <string name="workers_header_uptime">运行时间</string>
+    <string name="workers_header_last_seen">最后在线</string>
+    <string name="workers_label_session_count">%d 个会话</string>
+
+    <!-- Dashboard Screen -->
+    <string name="screen_title_dashboard">公共矿池</string>
+    <string name="dashboard_action_settings">设置</string>
+    <string name="dashboard_info_set_wallet">请在设置中设置您的比特币钱包地址以查看您的统计数据。</string>
+    <string name="dashboard_card_label_your_best_difficulty">您的最佳难度</string>
+    <string name="dashboard_card_label_network_difficulty">网络难度</string>
+    <string name="dashboard_card_label_network_hash_rate">网络哈希率</string>
+    <string name="dashboard_card_label_block_height">区块高度</string>
+    <string name="dashboard_card_secondary_block_weight_prefix">权重:</string>
+    <string name="dashboard_chart_title">哈希率历史记录</string>
+    <string name="dashboard_chart_no_data">此钱包没有可用的图表数据。</string>
+    <string name="dashboard_chart_set_wallet">设置钱包地址以查看图表。</string>
+    <string name="dashboard_chart_series_10min">10 分钟</string>
+    <string name="dashboard_chart_series_2hour">2 小时</string>
+</resources>

+ 1 - 7
app/src/main/res/values/colors.xml

@@ -1,10 +1,4 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <color name="purple_200">#FFBB86FC</color>
-    <color name="purple_500">#FF6200EE</color>
-    <color name="purple_700">#FF3700B3</color>
-    <color name="teal_200">#FF03DAC5</color>
-    <color name="teal_700">#FF018786</color>
-    <color name="black">#FF000000</color>
-    <color name="white">#FFFFFFFF</color>
+    <color name="app_background">#17212F</color>
 </resources>

+ 4 - 0
app/src/main/res/values/ic_launcher_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#249289</color>
+</resources>

+ 59 - 0
app/src/main/res/values/strings.xml

@@ -1,3 +1,62 @@
 <resources>
     <string name="app_name">PublicPool</string>
+
+    <!-- Tab Titles -->
+    <string name="tab_title_dashboard">Dashboard</string>
+    <string name="tab_title_workers">Workers</string>
+    <string name="tab_title_wallet">Wallet</string>
+
+    <!-- General -->
+    <string name="error_unknown">Unknown error</string>
+    <string name="text_placeholder_dash">-</string>
+    <string name="currency_usd">USD</string>
+    <string name="action_back">Back</string>
+
+    <!-- Settings Screen -->
+    <string name="screen_title_settings">Settings</string>
+    <string name="settings_label_wallet_address">Bitcoin Wallet Address</string>
+    <string name="settings_button_save">Save</string>
+    <string name="settings_toast_wallet_saved">Wallet address saved</string>
+
+    <!-- Wallet Screen -->
+    <string name="screen_title_wallet_details">Wallet Details</string>
+    <string name="wallet_error_not_set">Wallet address not set. Please configure in Settings.</string>
+    <string name="wallet_error_load_failed">Could not load wallet information.</string>
+    <string name="wallet_label_current_btc_price">Current BTC Price</string>
+    <string name="wallet_label_final_balance">Final Balance</string>
+    <string name="wallet_label_total_received">Total Received</string>
+    <string name="wallet_label_total_sent">Total Sent</string>
+    <string name="wallet_header_recent_transactions_count">Recent Transactions (%d)</string>
+    <string name="wallet_clipboard_label">Wallet Address</string>
+    <string name="wallet_toast_address_copied">Address copied to clipboard</string>
+    <string name="wallet_balance_fiat_prefix">≈</string>
+
+    <!-- Workers Screen -->
+    <string name="screen_title_workers">Workers</string>
+    <string name="workers_no_wallet">Please set your wallet address in the settings to view worker details.</string>
+    <string name="workers_no_data">No active workers found for this wallet address.</string>
+    <string name="content_description_expand">Expand worker details</string>
+    <string name="content_description_collapse">Collapse worker details</string>
+    <string name="workers_header_session_id">Session ID</string>
+    <string name="workers_header_hash_rate">Hash Rate</string>
+    <string name="workers_header_difficulty">Difficulty</string>
+    <string name="workers_header_uptime">Uptime</string>
+    <string name="workers_header_last_seen">Last Seen</string>
+    <string name="workers_label_session_count">%d Session(s)</string>
+
+    <!-- Dashboard Screen -->
+    <string name="screen_title_dashboard">Public Pool</string>
+    <string name="dashboard_action_settings">Settings</string>
+    <string name="dashboard_info_set_wallet">Please set your Bitcoin wallet address in Settings to see your stats.</string>
+    <string name="dashboard_card_label_your_best_difficulty">Your Best Difficulty</string>
+    <string name="dashboard_card_label_network_difficulty">Network Difficulty</string>
+    <string name="dashboard_card_label_network_hash_rate">Network Hash Rate</string>
+    <string name="dashboard_card_label_block_height">Block Height</string>
+    <string name="dashboard_card_secondary_block_weight_prefix">Weight:</string>
+    <string name="dashboard_chart_title">Hash Rate History</string>
+    <string name="dashboard_chart_no_data">No chart data available for this wallet.</string>
+    <string name="dashboard_chart_set_wallet">Set wallet address to see chart.</string>
+    <string name="dashboard_chart_series_10min">10 Minute</string>
+    <string name="dashboard_chart_series_2hour">2 Hour</string>
+
 </resources>

+ 14 - 0
app/src/main/res/values/themes.xml

@@ -2,4 +2,18 @@
 <resources>
 
     <style name="Theme.PublicPool" parent="android:Theme.Material.Light.NoActionBar" />
+
+    <!-- Splash Screen Theme -->
+    <style name="Theme.App.Starting" parent="Theme.SplashScreen">
+        <!-- Set the splash screen background color to AppBackground -->
+        <item name="windowSplashScreenBackground">@color/app_background</item>
+
+        <!-- Use windowSplashScreenAnimatedIcon to add animation -->
+        <!-- <item name="windowSplashScreenAnimatedIcon">@drawable/...</item> -->
+        <!-- Or use windowSplashScreenIconBackgroundColor + postSplashScreenTheme -->
+
+        <!-- Set the theme of the Activity that follows your splash screen. -->
+        <!-- This is required. -->
+        <item name="postSplashScreenTheme">@style/Theme.PublicPool</item>
+    </style>
 </resources>

+ 19 - 15
gradle/libs.versions.toml

@@ -1,27 +1,28 @@
 [versions]
 agp = "8.9.1"
-kotlin = "2.0.21"
+kotlin = "2.1.20"
 # --- Added Versions ---
-ksp = "1.9.23-1.0.20" # KSP version (check compatibility with Kotlin version)
-navigationCompose = "2.7.7"
+ksp = "2.1.20-1.0.32" # Updated KSP version for Kotlin 2.0.21
+navigationCompose = "2.8.9"
 ktor = "2.3.12" # Or latest stable
-room = "2.6.1"
 koin = "3.5.6"
 koinCompose = "3.5.6" # Ensure compatibility if using separate compose version
 anychart = "1.1.5" # Check for latest version
-datastore = "1.1.1"
+datastore = "1.1.4"
 coil = "2.6.0"
 logback = "1.3.14" # Or latest stable for Ktor logging
 kotlinxSerializationJson = "1.6.3" # Ensure compatibility
+voyager = "1.1.0-beta03" # Add Voyager version
 # --- End Added Versions ---
-coreKtx = "1.10.1"
+coreKtx = "1.15.0"
 junit = "4.13.2"
-junitVersion = "1.1.5"
-espressoCore = "3.5.1"
-lifecycleRuntimeKtx = "2.6.1"
-activityCompose = "1.8.0"
-composeBom = "2024.09.00"
+junitVersion = "1.2.1"
+espressoCore = "3.6.1"
+lifecycleRuntimeKtx = "2.8.7"
+activityCompose = "1.10.1"
+composeBom = "2025.03.01"
 composeCompiler = "1.5.11" # Explicitly define compiler version used in build.gradle
+coreSplashscreen = "1.0.1"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -48,10 +49,6 @@ ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serializatio
 ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" }
 logback-classic = { group = "ch.qos.logback", name = "logback-classic", version.ref = "logback" }
 kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } # Explicit dependency sometimes needed
-# Room
-androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
-androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
-androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
 # Koin
 koin-bom = { group = "io.insert-koin", name = "koin-bom", version.ref = "koin"}
 koin-android = { group = "io.insert-koin", name = "koin-android" } # Version from BOM
@@ -62,6 +59,13 @@ anychart-android-core = { group = "com.github.AnyChart", name = "AnyChart-Androi
 androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore"}
 # Coil
 coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil"}
+# Voyager
+voyager-navigator = { group = "cafe.adriel.voyager", name = "voyager-navigator", version.ref = "voyager" }
+# voyager-androidx = { group = "cafe.adriel.voyager", name = "voyager-androidx", version.ref = "voyager" }
+voyager-koin = { group = "cafe.adriel.voyager", name = "voyager-koin", version.ref = "voyager" }
+voyager-transitions = { group = "cafe.adriel.voyager", name = "voyager-transitions", version.ref = "voyager" }
+voyager-tab-navigator = { group = "cafe.adriel.voyager", name = "voyager-tab-navigator", version.ref = "voyager" } # Add TabNavigator
+androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "coreSplashscreen" }
 # --- End Added Libraries ---
 
 [plugins]

+ 155 - 0
privacy_policy.md

@@ -0,0 +1,155 @@
+# Privacy Policy
+
+Last updated: April 4, 2025
+
+This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.
+
+We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the Privacy Policy Generator.
+
+## Interpretation and Definitions
+
+### Interpretation
+
+The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
+
+### Definitions
+
+For the purposes of this Privacy Policy:
+
+*   **Account** means a unique account created for You to access our Service or parts of our Service.
+*   **Affiliate** means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.
+*   **Application** refers to Weekly Weather, the software program provided by the Company.
+*   **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Weekly Weather.
+*   **Country** refers to: Spain
+*   **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet.
+*   **Personal Data** is any information that relates to an identified or identifiable individual.
+*   **Service** refers to the Application.
+*   **Service Provider** means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
+*   **Usage Data** refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
+*   **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
+
+## Collecting and Using Your Personal Data
+
+### Types of Data Collected
+
+#### Personal Data
+
+While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:
+
+*   Usage Data
+
+#### Usage Data
+
+Usage Data is collected automatically when using the Service.
+
+Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.
+
+When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.
+
+We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.
+
+#### Information Collected while Using the Application
+
+While using Our Application, in order to provide features of Our Application, We may collect, with Your prior permission:
+
+*   Information regarding your location
+
+We use this information to provide features of Our Service, to improve and customize Our Service. The information may be uploaded to the Company's servers and/or a Service Provider's server or it may be simply stored on Your device.
+
+You can enable or disable access to this information at any time, through Your Device settings.
+
+### Use of Your Personal Data
+
+The Company may use Personal Data for the following purposes:
+
+*   **To provide and maintain our Service**, including to monitor the usage of our Service.
+*   **To manage Your Account:** to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.
+*   **For the performance of a contract:** the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.
+*   **To contact You:** To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.
+*   **To provide You** with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.
+*   **To manage Your requests:** To attend and manage Your requests to Us.
+*   **For business transfers:** We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.
+*   **For other purposes**: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.
+
+We may share Your personal information in the following situations:
+
+*   **With Service Providers:** We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You.
+*   **For business transfers:** We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company.
+*   **With Affiliates:** We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.
+*   **With business partners:** We may share Your information with Our business partners to offer You certain products, services or promotions.
+*   **With other users:** when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside.
+*   **With Your consent**: We may disclose Your personal information for any other purpose with Your consent.
+
+### Retention of Your Personal Data
+
+The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
+
+The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.
+
+### Transfer of Your Personal Data
+
+Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.
+
+Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.
+
+The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.
+
+### Delete Your Personal Data
+
+You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You.
+
+Our Service may give You the ability to delete certain information about You from within the Service.
+
+You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us.
+
+Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so.
+
+### Disclosure of Your Personal Data
+
+#### Business Transactions
+
+If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.
+
+#### Law enforcement
+
+Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).
+
+#### Other legal requirements
+
+The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:
+
+*   Comply with a legal obligation
+*   Protect and defend the rights or property of the Company
+*   Prevent or investigate possible wrongdoing in connection with the Service
+*   Protect the personal safety of Users of the Service or the public
+*   Protect against legal liability
+
+### Security of Your Personal Data
+
+The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.
+
+## Children's Privacy
+
+Our Service does not address anyone under the age of 13\. We do not knowingly collect personally identifiable information from anyone under the age of 13\. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.
+
+If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.
+
+## Links to Other Websites
+
+Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.
+
+We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
+
+## Changes to this Privacy Policy
+
+We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.
+
+We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.
+
+You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
+
+## Contact Us
+
+If you have any questions about this Privacy Policy, You can contact us:
+
+*   By visiting this page on our website: <https://github.com/codeskraps/Weekly-Weather/> 

+ 2 - 0
settings.gradle.kts

@@ -8,6 +8,7 @@ pluginManagement {
             }
         }
         mavenCentral()
+        maven { url = uri("https://jitpack.io") }
         gradlePluginPortal()
     }
 }
@@ -16,6 +17,7 @@ dependencyResolutionManagement {
     repositories {
         google()
         mavenCentral()
+        maven { url = uri("https://jitpack.io") }
     }
 }
 

Некоторые файлы не были показаны из-за большого количества измененных файлов