Merge pull request 'dev' (#26) from dev into main
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Gitea Android Builder / Build (push) Successful in 3m29s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Gitea Android Builder / Build (push) Successful in 3m29s
				
			Reviewed-on: #26
This commit is contained in:
		| @@ -49,6 +49,7 @@ dependencies { | |||||||
|     implementation(libs.androidx.ui.graphics) |     implementation(libs.androidx.ui.graphics) | ||||||
|     implementation(libs.androidx.ui.tooling.preview) |     implementation(libs.androidx.ui.tooling.preview) | ||||||
|     implementation(libs.androidx.material3) |     implementation(libs.androidx.material3) | ||||||
|  |     implementation(libs.androidx.fragment.ktx) | ||||||
|     testImplementation(libs.junit) |     testImplementation(libs.junit) | ||||||
|     androidTestImplementation(libs.androidx.junit) |     androidTestImplementation(libs.androidx.junit) | ||||||
|     androidTestImplementation(libs.androidx.espresso.core) |     androidTestImplementation(libs.androidx.espresso.core) | ||||||
| @@ -62,5 +63,6 @@ dependencies { | |||||||
|     implementation(libs.kotlinx.coroutines.android) |     implementation(libs.kotlinx.coroutines.android) | ||||||
|     implementation(libs.kotlinx.coroutines.core) |     implementation(libs.kotlinx.coroutines.core) | ||||||
|     implementation(libs.vico.compose.m3) |     implementation(libs.vico.compose.m3) | ||||||
|  |     implementation(libs.cicerone) | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -3,6 +3,7 @@ | |||||||
|     xmlns:tools="http://schemas.android.com/tools"> |     xmlns:tools="http://schemas.android.com/tools"> | ||||||
|     <uses-permission android:name="android.permission.INTERNET" /> |     <uses-permission android:name="android.permission.INTERNET" /> | ||||||
|     <application |     <application | ||||||
|  |         android:name=".App" | ||||||
|         android:allowBackup="false" |         android:allowBackup="false" | ||||||
|         android:dataExtractionRules="@xml/data_extraction_rules" |         android:dataExtractionRules="@xml/data_extraction_rules" | ||||||
|         android:fullBackupContent="@xml/backup_rules" |         android:fullBackupContent="@xml/backup_rules" | ||||||
|   | |||||||
| @@ -0,0 +1,119 @@ | |||||||
|  | package ru.vendetti.bitcoin_summarizer | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.rememberScrollState | ||||||
|  | import androidx.compose.foundation.verticalScroll | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.Menu | ||||||
|  | import androidx.compose.material.icons.filled.MoreVert | ||||||
|  | import androidx.compose.material3.CenterAlignedTopAppBar | ||||||
|  | import androidx.compose.material3.DrawerValue | ||||||
|  | import androidx.compose.material3.ExperimentalMaterial3Api | ||||||
|  | import androidx.compose.material3.Icon | ||||||
|  | import androidx.compose.material3.IconButton | ||||||
|  | import androidx.compose.material3.MaterialTheme | ||||||
|  | import androidx.compose.material3.MediumTopAppBar | ||||||
|  | import androidx.compose.material3.ModalNavigationDrawer | ||||||
|  | import androidx.compose.material3.Scaffold | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.material3.TopAppBarDefaults | ||||||
|  | import androidx.compose.material3.rememberDrawerState | ||||||
|  | import androidx.compose.material3.rememberTopAppBarState | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.rememberCoroutineScope | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||||
|  | import androidx.compose.ui.platform.ComposeView | ||||||
|  | import androidx.compose.ui.text.style.TextOverflow | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme | ||||||
|  |  | ||||||
|  | class AboutFragment: Fragment() { | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         return inflater.inflate(R.layout.fragment_compose_view, | ||||||
|  |             container, | ||||||
|  |             false).apply { | ||||||
|  |             findViewById<ComposeView>(R.id.compose_view).setContent { | ||||||
|  |                 BitcoinSummarizerTheme { | ||||||
|  |                     AboutComposable() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalMaterial3Api::class) | ||||||
|  | @Composable | ||||||
|  | @Preview | ||||||
|  | fun AboutComposable() { | ||||||
|  |     val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) | ||||||
|  |     val scope = rememberCoroutineScope() | ||||||
|  |  | ||||||
|  |     ModalNavigationDrawer( | ||||||
|  |         drawerState = drawerState, | ||||||
|  |         drawerContent = { | ||||||
|  |             MyHamburgerModal(ScreenKey.About) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         Scaffold( | ||||||
|  |             modifier = Modifier, | ||||||
|  |  | ||||||
|  |             topBar = { | ||||||
|  |                 CenterAlignedTopAppBar( | ||||||
|  |                     colors = TopAppBarDefaults.topAppBarColors( | ||||||
|  |                         containerColor = MaterialTheme.colorScheme.primary, | ||||||
|  |                         scrolledContainerColor = MaterialTheme.colorScheme.primary, | ||||||
|  |                         titleContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                         navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                         actionIconContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                     ), | ||||||
|  |                     title = { | ||||||
|  |                         Text( | ||||||
|  |                             "About", | ||||||
|  |                             maxLines = 1, | ||||||
|  |                             overflow = TextOverflow.Ellipsis | ||||||
|  |                         ) | ||||||
|  |                     }, | ||||||
|  |                     navigationIcon = { | ||||||
|  |                         IconButton(onClick = { | ||||||
|  |                             scope.launch { | ||||||
|  |                                 drawerState.apply { | ||||||
|  |                                     if(isClosed) open() else close() | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }) { | ||||||
|  |                             Icon( | ||||||
|  |                                 imageVector = Icons.Filled.Menu, | ||||||
|  |                                 contentDescription = "Navigation hamburger menu" | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) | ||||||
|  |                 ) | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         { innerPadding -> | ||||||
|  |             Box( | ||||||
|  |                 modifier = Modifier | ||||||
|  |                     .background(Color.Transparent) | ||||||
|  |                     .padding(innerPadding) | ||||||
|  |                     .verticalScroll(rememberScrollState()) | ||||||
|  |             ) { | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								app/src/main/java/ru/vendetti/bitcoin_summarizer/App.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/src/main/java/ru/vendetti/bitcoin_summarizer/App.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | package ru.vendetti.bitcoin_summarizer | ||||||
|  |  | ||||||
|  | import android.app.Application | ||||||
|  | import android.os.Debug | ||||||
|  | import android.util.Log | ||||||
|  | import com.github.terrakok.cicerone.Cicerone | ||||||
|  |  | ||||||
|  | class App : Application() { | ||||||
|  |     private val cicerone = Cicerone.create() | ||||||
|  |     val router get() = cicerone.router | ||||||
|  |     val navigatorHolder get() = cicerone.getNavigatorHolder() | ||||||
|  |  | ||||||
|  |     override fun onCreate() { | ||||||
|  |         super.onCreate() | ||||||
|  |         INSTANCE = this | ||||||
|  |         Log.println(Log.DEBUG, "App", "Instance is $INSTANCE") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     companion object { | ||||||
|  |         internal lateinit var INSTANCE: App | ||||||
|  |             private set | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,411 @@ | |||||||
|  | package ru.vendetti.bitcoin_summarizer | ||||||
|  |  | ||||||
|  | import android.annotation.SuppressLint | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.Column | ||||||
|  | import androidx.compose.foundation.layout.Spacer | ||||||
|  | import androidx.compose.foundation.layout.height | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.rememberScrollState | ||||||
|  | import androidx.compose.foundation.verticalScroll | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.Menu | ||||||
|  | import androidx.compose.material.icons.filled.MoreVert | ||||||
|  | import androidx.compose.material3.CenterAlignedTopAppBar | ||||||
|  | import androidx.compose.material3.DrawerValue | ||||||
|  | import androidx.compose.material3.ExperimentalMaterial3Api | ||||||
|  | import androidx.compose.material3.HorizontalDivider | ||||||
|  | import androidx.compose.material3.Icon | ||||||
|  | import androidx.compose.material3.IconButton | ||||||
|  | import androidx.compose.material3.MaterialTheme | ||||||
|  | import androidx.compose.material3.MediumTopAppBar | ||||||
|  | import androidx.compose.material3.ModalDrawerSheet | ||||||
|  | import androidx.compose.material3.ModalNavigationDrawer | ||||||
|  | import androidx.compose.material3.Scaffold | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.material3.TopAppBarDefaults | ||||||
|  | import androidx.compose.material3.rememberDrawerState | ||||||
|  | import androidx.compose.material3.rememberTopAppBarState | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.LaunchedEffect | ||||||
|  | import androidx.compose.runtime.getValue | ||||||
|  | import androidx.compose.runtime.mutableIntStateOf | ||||||
|  | import androidx.compose.runtime.mutableStateOf | ||||||
|  | import androidx.compose.runtime.remember | ||||||
|  | import androidx.compose.runtime.rememberCoroutineScope | ||||||
|  | import androidx.compose.runtime.setValue | ||||||
|  | import androidx.compose.ui.Alignment | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||||
|  | import androidx.compose.ui.platform.ComposeView | ||||||
|  | import androidx.compose.ui.text.style.TextOverflow | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import androidx.compose.ui.unit.sp | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLineComponent | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLine | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.rememberVicoScrollState | ||||||
|  | import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState | ||||||
|  | import com.patrykandpatrick.vico.compose.common.component.rememberTextComponent | ||||||
|  | import com.patrykandpatrick.vico.compose.common.component.shapeComponent | ||||||
|  | import com.patrykandpatrick.vico.compose.common.fill | ||||||
|  | import com.patrykandpatrick.vico.compose.common.shape.rounded | ||||||
|  | import com.patrykandpatrick.vico.compose.common.vicoTheme | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.Zoom | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.data.CartesianLayerRangeProvider | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.data.lineSeries | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.decoration.HorizontalLine | ||||||
|  | import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer | ||||||
|  | import com.patrykandpatrick.vico.core.common.Position | ||||||
|  | import com.patrykandpatrick.vico.core.common.component.LineComponent | ||||||
|  | import com.patrykandpatrick.vico.core.common.component.TextComponent | ||||||
|  | import com.patrykandpatrick.vico.core.common.shape.CorneredShape | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme | ||||||
|  | import ru.vendetti.bitcoin_summarizer.ui.theme.Flame | ||||||
|  | import ru.vendetti.bitcoin_summarizer.ui.theme.Green2 | ||||||
|  | import java.text.DecimalFormat | ||||||
|  | import java.time.Instant | ||||||
|  | import java.time.format.DateTimeFormatter | ||||||
|  |  | ||||||
|  | class CryptoFragment: Fragment() { | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         return inflater.inflate(R.layout.fragment_compose_view, | ||||||
|  |             container, | ||||||
|  |             false).apply { | ||||||
|  |             findViewById<ComposeView>(R.id.compose_view).setContent { | ||||||
|  |                 BitcoinSummarizerTheme { | ||||||
|  |                     CryptoComposable() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @SuppressLint("MutableCollectionMutableState", "SimpleDateFormat") | ||||||
|  | @OptIn(ExperimentalMaterial3Api::class) | ||||||
|  | @Preview | ||||||
|  | @Composable | ||||||
|  | fun CryptoComposable() { | ||||||
|  |     // Создаем репозиторий для работы с API | ||||||
|  |     val cryptoRepository = remember { CryptoRepository(RetrofitClient.apiService) } | ||||||
|  |     // Состояния для хранения результатов запросов | ||||||
|  |     var bitcoinTicker by remember { mutableStateOf(TickerData()) } | ||||||
|  |     var globalData by remember { mutableStateOf(GlobalResponse()) } | ||||||
|  |     var fearGreedDataList by remember { mutableStateOf(ArrayList<FearAndGreedData>()) } | ||||||
|  |  | ||||||
|  |     var fearGreedIndexDaysCount by remember { mutableIntStateOf(30) } | ||||||
|  |  | ||||||
|  |     // Запускаем корутину для выполнения сетевых запросов | ||||||
|  |     LaunchedEffect(fearGreedIndexDaysCount) { | ||||||
|  |         try { | ||||||
|  |             // Запрос Bitcoin Ticker | ||||||
|  |             val tickerResponse = cryptoRepository.fetchBitcoinTicker() | ||||||
|  |             bitcoinTicker = tickerResponse!!.first() | ||||||
|  |  | ||||||
|  |             // Запрос глобальных данных | ||||||
|  |             val globalResponse = cryptoRepository.fetchGlobalData() | ||||||
|  |             globalData = globalResponse!! | ||||||
|  |  | ||||||
|  |             // Запрос индекса страха и жадности | ||||||
|  |             val fearResponse = cryptoRepository.fetchFearAndGreedData(fearGreedIndexDaysCount) | ||||||
|  |             fearGreedDataList = fearResponse?.dataList as ArrayList<FearAndGreedData> | ||||||
|  |             fearGreedDataList.reverse() | ||||||
|  |         }catch (e: Exception) { | ||||||
|  |             bitcoinTicker = TickerData( | ||||||
|  |                 id = "", | ||||||
|  |                 name = "", | ||||||
|  |                 symbol = "", | ||||||
|  |                 rank = "", | ||||||
|  |                 priceUsd = "", | ||||||
|  |                 priceBtc = "", | ||||||
|  |                 volume24hUsd = "", | ||||||
|  |                 marketCapUsd = "", | ||||||
|  |                 availableSupply = "", | ||||||
|  |                 totalSupply = "", | ||||||
|  |                 maxSupply = "", | ||||||
|  |                 percentChange1h = "", | ||||||
|  |                 percentChange24h = "", | ||||||
|  |                 percentChange7d = "", | ||||||
|  |                 lastUpdated = "" | ||||||
|  |             ) | ||||||
|  |             globalData = GlobalResponse( | ||||||
|  |                 activeCryptocurrencies = "", | ||||||
|  |                 totalMarketCapUsd = "", | ||||||
|  |                 total24hVolumeUsd = "", | ||||||
|  |                 bitcoinPercentageOfMarketCap = "" | ||||||
|  |             ) | ||||||
|  |             fearGreedDataList = ArrayList<FearAndGreedData>() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val modelProducer = remember { CartesianChartModelProducer() } | ||||||
|  |  | ||||||
|  |     LaunchedEffect(fearGreedDataList) { | ||||||
|  |         modelProducer.runTransaction { | ||||||
|  |             var numberValues = Array(fearGreedDataList.count()) { | ||||||
|  |                     index -> | ||||||
|  |                 fearGreedDataList[index] | ||||||
|  |                     .value.toInt() | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if(numberValues.isEmpty()) | ||||||
|  |                 numberValues = Array(1) {0} | ||||||
|  |  | ||||||
|  |             lineSeries { series(numberValues.toList()) } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) | ||||||
|  |  | ||||||
|  |     // Отображаем результаты на странице | ||||||
|  |  | ||||||
|  |     val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) | ||||||
|  |     val scope = rememberCoroutineScope() | ||||||
|  |  | ||||||
|  |     ModalNavigationDrawer( | ||||||
|  |         drawerState = drawerState, | ||||||
|  |         drawerContent = { | ||||||
|  |             MyHamburgerModal(ScreenKey.Crypto) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         Scaffold( | ||||||
|  |             modifier = Modifier | ||||||
|  |                 .nestedScroll(scrollBehavior.nestedScrollConnection), | ||||||
|  |  | ||||||
|  |             topBar = { | ||||||
|  |                 CenterAlignedTopAppBar( | ||||||
|  |                     colors = TopAppBarDefaults.topAppBarColors( | ||||||
|  |                         containerColor = MaterialTheme.colorScheme.primary, | ||||||
|  |                         scrolledContainerColor = MaterialTheme.colorScheme.primary, | ||||||
|  |                         titleContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                         navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                         actionIconContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                     ), | ||||||
|  |                     title = { | ||||||
|  |                         Text( | ||||||
|  |                             "Crypto statistics", | ||||||
|  |                             maxLines = 1, | ||||||
|  |                             overflow = TextOverflow.Ellipsis | ||||||
|  |                         ) | ||||||
|  |                     }, | ||||||
|  |                     navigationIcon = { | ||||||
|  |                         IconButton(onClick = { | ||||||
|  |                             scope.launch { | ||||||
|  |                                 drawerState.apply { | ||||||
|  |                                     if(isClosed) open() else close() | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }) { | ||||||
|  |                             Icon( | ||||||
|  |                                 imageVector = Icons.Filled.Menu, | ||||||
|  |                                 contentDescription = "Navigation hamburger menu" | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     scrollBehavior = scrollBehavior | ||||||
|  |                 ) | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         { innerPadding -> | ||||||
|  |             Box( | ||||||
|  |                 modifier = Modifier | ||||||
|  |                     .background(Color.Transparent) | ||||||
|  |                     .padding(innerPadding) | ||||||
|  |                     .verticalScroll(rememberScrollState()) | ||||||
|  |             ) { | ||||||
|  |                 Column( | ||||||
|  |                     modifier = Modifier | ||||||
|  |                         .padding(16.dp, 16.dp, 16.dp, 36.dp) | ||||||
|  |                 ) { | ||||||
|  |                     /* Fear Greed Chart Start */ | ||||||
|  |                     Spacer(modifier = Modifier.height(16.dp)) | ||||||
|  |                     Text( | ||||||
|  |                         "Индекс страха/жадности", | ||||||
|  |                         modifier = Modifier | ||||||
|  |                             .align(alignment = Alignment.CenterHorizontally), | ||||||
|  |                         fontSize = 24.sp | ||||||
|  |                     ) | ||||||
|  |                     Spacer(modifier = Modifier.height(16.dp)) | ||||||
|  |                     if (fearGreedDataList.count() > 1) { | ||||||
|  |                         CartesianChartHost( | ||||||
|  |                             zoomState = rememberVicoZoomState( | ||||||
|  |                                 zoomEnabled = true, | ||||||
|  |                                 initialZoom = Zoom.x(fearGreedIndexDaysCount.toDouble()), | ||||||
|  |                             ), | ||||||
|  |                             scrollState = rememberVicoScrollState(scrollEnabled = true), | ||||||
|  |                             chart = rememberCartesianChart( | ||||||
|  |                                 rememberLineCartesianLayer( | ||||||
|  |                                     lineProvider = LineCartesianLayer.LineProvider.series( | ||||||
|  |                                         vicoTheme.lineCartesianLayerColors.map { _ -> | ||||||
|  |                                             LineCartesianLayer | ||||||
|  |                                                 .rememberLine( | ||||||
|  |                                                     LineCartesianLayer.LineFill.single( | ||||||
|  |                                                         fill(MaterialTheme.colorScheme.onPrimary) | ||||||
|  |                                                     ) | ||||||
|  |                                                 ) | ||||||
|  |                                         } | ||||||
|  |                                     ), | ||||||
|  |                                     rangeProvider = remember { | ||||||
|  |                                         CartesianLayerRangeProvider.fixed( | ||||||
|  |                                             minY = 0.0, | ||||||
|  |                                             maxY = 100.0, | ||||||
|  |                                         ) | ||||||
|  |                                     } | ||||||
|  |                                 ), | ||||||
|  |                                 startAxis = VerticalAxis.rememberStart( | ||||||
|  |                                     title = "FGI", | ||||||
|  |                                     titleComponent = rememberTextComponent(MaterialTheme.colorScheme.onPrimary), | ||||||
|  |                                     line = rememberAxisLineComponent(fill(MaterialTheme.colorScheme.onPrimary)), | ||||||
|  |                                     label = rememberAxisLabelComponent(MaterialTheme.colorScheme.onPrimary) | ||||||
|  |                                 ), | ||||||
|  |                                 bottomAxis = HorizontalAxis.rememberBottom( | ||||||
|  |                                     valueFormatter = { _, value, _ -> | ||||||
|  |                                         val date = DateTimeFormatter.ISO_INSTANT | ||||||
|  |                                             .format(Instant.ofEpochSecond(fearGreedDataList[value.toInt()].timestamp.toLong())) | ||||||
|  |                                         val dateComponents = date.split("T")[0].split("-") | ||||||
|  |                                         // returns dd.mm | ||||||
|  |                                         "${dateComponents[2]}.${dateComponents[1]}" | ||||||
|  |                                     }, | ||||||
|  |                                     title = "last $fearGreedIndexDaysCount days", | ||||||
|  |                                     titleComponent = rememberTextComponent(MaterialTheme.colorScheme.onPrimary), | ||||||
|  |                                     line = rememberAxisLineComponent(fill(MaterialTheme.colorScheme.onPrimary)), | ||||||
|  |                                     label = rememberAxisLabelComponent(MaterialTheme.colorScheme.onPrimary) | ||||||
|  |                                 ), | ||||||
|  |                                 decorations = listOf( | ||||||
|  |                                     remember { | ||||||
|  |                                         HorizontalLine( | ||||||
|  |                                             y = { 25.toDouble() }, | ||||||
|  |                                             line = LineComponent(fill(Flame), 1f), | ||||||
|  |                                             labelComponent = TextComponent( | ||||||
|  |                                                 background = | ||||||
|  |                                                     shapeComponent( | ||||||
|  |                                                         fill(Flame), | ||||||
|  |                                                         CorneredShape.rounded( | ||||||
|  |                                                             topLeft = 4.dp, | ||||||
|  |                                                             topRight = 4.dp | ||||||
|  |                                                         ) | ||||||
|  |                                                     ), | ||||||
|  |                                             ), | ||||||
|  |                                             label = { "Страх" }, | ||||||
|  |                                             verticalLabelPosition = Position.Vertical.Top | ||||||
|  |                                         ) | ||||||
|  |                                     }, | ||||||
|  |                                     remember { | ||||||
|  |                                         HorizontalLine( | ||||||
|  |                                             y = { 70.toDouble() }, | ||||||
|  |                                             line = LineComponent(fill(Green2), 1f), | ||||||
|  |                                             labelComponent = TextComponent( | ||||||
|  |                                                 background = | ||||||
|  |                                                     shapeComponent( | ||||||
|  |                                                         fill(Green2), | ||||||
|  |                                                         CorneredShape.rounded( | ||||||
|  |                                                             bottomLeft = 4.dp, | ||||||
|  |                                                             bottomRight = 4.dp | ||||||
|  |                                                         ) | ||||||
|  |                                                     ), | ||||||
|  |                                             ), | ||||||
|  |                                             label = { "Жадность" }, | ||||||
|  |                                             verticalLabelPosition = Position.Vertical.Bottom | ||||||
|  |                                         ) | ||||||
|  |                                     } | ||||||
|  |                                 ), | ||||||
|  |                             ), | ||||||
|  |                             modelProducer = modelProducer, | ||||||
|  |                         ) | ||||||
|  |                     } else { | ||||||
|  |                         Text("Загрузка...") | ||||||
|  |                     } | ||||||
|  |                     /* Fear Greed Chart End */ | ||||||
|  |                     Spacer(modifier = Modifier.height(16.dp)) | ||||||
|  |                     HorizontalDivider(thickness = 2.dp) | ||||||
|  |                     Spacer(modifier = Modifier.height(16.dp)) | ||||||
|  |                     Text( | ||||||
|  |                         "Данные о Биткоине", | ||||||
|  |                         modifier = Modifier | ||||||
|  |                             .align(alignment = Alignment.CenterHorizontally), | ||||||
|  |                         fontSize = 24.sp | ||||||
|  |                     ) | ||||||
|  |                     Spacer(modifier = Modifier.height(16.dp)) | ||||||
|  |                     val decimalFormatter = DecimalFormat("0.00") | ||||||
|  |                     val largeNumberFormatter = DecimalFormat("#,###") | ||||||
|  |  | ||||||
|  |                     if (bitcoinTicker.lastUpdated.isNotEmpty()) { | ||||||
|  |                         Text("Текущая цена: \n \$ ${decimalFormatter.format(bitcoinTicker.priceUsd.toFloat())}\n") | ||||||
|  |                         Text("Суточный оборот: \n \$ ${largeNumberFormatter.format(bitcoinTicker.volume24hUsd.toFloat())}\n") | ||||||
|  |                         Text("Капитализация: \n \$ ${largeNumberFormatter.format(bitcoinTicker.marketCapUsd.toFloat())}\n") | ||||||
|  |                         Text( | ||||||
|  |                             "Изменение курса за: " + | ||||||
|  |                                     "\n Сутки: ${decimalFormatter.format(bitcoinTicker.percentChange24h.toFloat())}% " + | ||||||
|  |                                     "\n Неделю: ${decimalFormatter.format(bitcoinTicker.percentChange7d.toFloat())}%\n" | ||||||
|  |                         ) | ||||||
|  |  | ||||||
|  |                         val humanDate = DateTimeFormatter.ISO_INSTANT | ||||||
|  |                             .format(Instant.ofEpochSecond(bitcoinTicker.lastUpdated.toLong())) | ||||||
|  |  | ||||||
|  |                         Text( | ||||||
|  |                             "Время последнего обновления: \n ${humanDate.split("T")[0]}, " + | ||||||
|  |                                     "${ | ||||||
|  |                                         humanDate.split("T")[1].replace( | ||||||
|  |                                             Regex(":[0-9]+[A-Z]"), | ||||||
|  |                                             "" | ||||||
|  |                                         ) | ||||||
|  |                                     }\n" | ||||||
|  |                         ) | ||||||
|  |                     } else { | ||||||
|  |                         Text("Загрузка...") | ||||||
|  |                     } | ||||||
|  |                     HorizontalDivider(thickness = 2.dp) | ||||||
|  |                     Spacer(modifier = Modifier.height(16.dp)) | ||||||
|  |                     Text( | ||||||
|  |                         "Глобальные данные", | ||||||
|  |                         modifier = Modifier | ||||||
|  |                             .align(alignment = Alignment.CenterHorizontally), | ||||||
|  |                         fontSize = 24.sp | ||||||
|  |                     ) | ||||||
|  |                     Spacer(modifier = Modifier.height(16.dp)) | ||||||
|  |                     if (globalData.totalMarketCapUsd.isNotEmpty()) { | ||||||
|  |                         Text( | ||||||
|  |                             "Общая капитализация крипторынка: \n \$ ${ | ||||||
|  |                                 largeNumberFormatter.format( | ||||||
|  |                                     globalData.totalMarketCapUsd.toFloat() | ||||||
|  |                                 ) | ||||||
|  |                             }\n" | ||||||
|  |                         ) | ||||||
|  |                         Text("Всего тикеров: \n ${globalData.activeCryptocurrencies}\n") | ||||||
|  |                         Text( | ||||||
|  |                             "Суточный оборот всех криптовалют: \n \$ ${ | ||||||
|  |                                 largeNumberFormatter.format( | ||||||
|  |                                     globalData.total24hVolumeUsd.toFloat() | ||||||
|  |                                 ) | ||||||
|  |                             }\n" | ||||||
|  |                         ) | ||||||
|  |                         Text("Процент доминации Биткоина: \n ${decimalFormatter.format(globalData.bitcoinPercentageOfMarketCap.toFloat())}%\n") | ||||||
|  |                     } else { | ||||||
|  |                         Text("Загрузка...") | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,119 @@ | |||||||
|  | package ru.vendetti.bitcoin_summarizer | ||||||
|  |  | ||||||
|  | import android.os.Bundle | ||||||
|  | import android.view.LayoutInflater | ||||||
|  | import android.view.View | ||||||
|  | import android.view.ViewGroup | ||||||
|  | import androidx.compose.foundation.background | ||||||
|  | import androidx.compose.foundation.layout.Box | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.foundation.rememberScrollState | ||||||
|  | import androidx.compose.foundation.verticalScroll | ||||||
|  | import androidx.compose.material.icons.Icons | ||||||
|  | import androidx.compose.material.icons.filled.Menu | ||||||
|  | import androidx.compose.material.icons.filled.MoreVert | ||||||
|  | import androidx.compose.material3.CenterAlignedTopAppBar | ||||||
|  | import androidx.compose.material3.DrawerValue | ||||||
|  | import androidx.compose.material3.ExperimentalMaterial3Api | ||||||
|  | import androidx.compose.material3.Icon | ||||||
|  | import androidx.compose.material3.IconButton | ||||||
|  | import androidx.compose.material3.MaterialTheme | ||||||
|  | import androidx.compose.material3.MediumTopAppBar | ||||||
|  | import androidx.compose.material3.ModalNavigationDrawer | ||||||
|  | import androidx.compose.material3.Scaffold | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.material3.TopAppBarDefaults | ||||||
|  | import androidx.compose.material3.rememberDrawerState | ||||||
|  | import androidx.compose.material3.rememberTopAppBarState | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.runtime.rememberCoroutineScope | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.graphics.Color | ||||||
|  | import androidx.compose.ui.input.nestedscroll.nestedScroll | ||||||
|  | import androidx.compose.ui.platform.ComposeView | ||||||
|  | import androidx.compose.ui.text.style.TextOverflow | ||||||
|  | import androidx.compose.ui.tooling.preview.Preview | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import kotlinx.coroutines.launch | ||||||
|  | import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme | ||||||
|  |  | ||||||
|  | class LearnFragment: Fragment() { | ||||||
|  |     override fun onCreateView( | ||||||
|  |         inflater: LayoutInflater, | ||||||
|  |         container: ViewGroup?, | ||||||
|  |         savedInstanceState: Bundle? | ||||||
|  |     ): View { | ||||||
|  |         return inflater.inflate(R.layout.fragment_compose_view, | ||||||
|  |             container, | ||||||
|  |             false).apply { | ||||||
|  |             findViewById<ComposeView>(R.id.compose_view).setContent { | ||||||
|  |                 BitcoinSummarizerTheme { | ||||||
|  |                     LearnComposable() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @OptIn(ExperimentalMaterial3Api::class) | ||||||
|  | @Composable | ||||||
|  | @Preview | ||||||
|  | fun LearnComposable() { | ||||||
|  |     val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) | ||||||
|  |     val scope = rememberCoroutineScope() | ||||||
|  |  | ||||||
|  |     ModalNavigationDrawer( | ||||||
|  |         drawerState = drawerState, | ||||||
|  |         drawerContent = { | ||||||
|  |             MyHamburgerModal(ScreenKey.Learn) | ||||||
|  |         } | ||||||
|  |     ) { | ||||||
|  |         Scaffold( | ||||||
|  |             modifier = Modifier, | ||||||
|  |  | ||||||
|  |             topBar = { | ||||||
|  |                 CenterAlignedTopAppBar( | ||||||
|  |                     colors = TopAppBarDefaults.topAppBarColors( | ||||||
|  |                         containerColor = MaterialTheme.colorScheme.primary, | ||||||
|  |                         scrolledContainerColor = MaterialTheme.colorScheme.primary, | ||||||
|  |                         titleContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                         navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                         actionIconContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |                     ), | ||||||
|  |                     title = { | ||||||
|  |                         Text( | ||||||
|  |                             "Learn", | ||||||
|  |                             maxLines = 1, | ||||||
|  |                             overflow = TextOverflow.Ellipsis | ||||||
|  |                         ) | ||||||
|  |                     }, | ||||||
|  |                     navigationIcon = { | ||||||
|  |                         IconButton(onClick = { | ||||||
|  |                             scope.launch { | ||||||
|  |                                 drawerState.apply { | ||||||
|  |                                     if(isClosed) open() else close() | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }) { | ||||||
|  |                             Icon( | ||||||
|  |                                 imageVector = Icons.Filled.Menu, | ||||||
|  |                                 contentDescription = "Navigation hamburger menu" | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) | ||||||
|  |                 ) | ||||||
|  |             }, | ||||||
|  |         ) | ||||||
|  |         { innerPadding -> | ||||||
|  |             Box( | ||||||
|  |                 modifier = Modifier | ||||||
|  |                     .background(Color.Transparent) | ||||||
|  |                     .padding(innerPadding) | ||||||
|  |                     .verticalScroll(rememberScrollState()) | ||||||
|  |             ) { | ||||||
|  |  | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ package ru.vendetti.bitcoin_summarizer | |||||||
|  |  | ||||||
| import android.annotation.SuppressLint | import android.annotation.SuppressLint | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
|  | import android.widget.FrameLayout | ||||||
| import androidx.activity.ComponentActivity | import androidx.activity.ComponentActivity | ||||||
| import androidx.activity.compose.setContent | import androidx.activity.compose.setContent | ||||||
| import androidx.compose.foundation.background | import androidx.compose.foundation.background | ||||||
| @@ -40,6 +41,8 @@ import androidx.compose.ui.text.style.TextOverflow | |||||||
| import androidx.compose.ui.tooling.preview.Preview | import androidx.compose.ui.tooling.preview.Preview | ||||||
| import androidx.compose.ui.unit.dp | import androidx.compose.ui.unit.dp | ||||||
| import androidx.compose.ui.unit.sp | import androidx.compose.ui.unit.sp | ||||||
|  | import androidx.fragment.app.FragmentActivity | ||||||
|  | import com.github.terrakok.cicerone.androidx.AppNavigator | ||||||
| import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost | import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost | ||||||
| import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent | import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLabelComponent | ||||||
| import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLineComponent | import com.patrykandpatrick.vico.compose.cartesian.axis.rememberAxisLineComponent | ||||||
| @@ -72,234 +75,25 @@ import ru.vendetti.bitcoin_summarizer.ui.theme.Green2 | |||||||
| import java.text.DecimalFormat | import java.text.DecimalFormat | ||||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||||
|  |  | ||||||
| class MainActivity : ComponentActivity() { | class MainActivity : FragmentActivity() { | ||||||
|  |  | ||||||
|  |     private val navigator = AppNavigator(this, R.id.container) | ||||||
|  |  | ||||||
|     override fun onCreate(savedInstanceState: Bundle?) { |     override fun onCreate(savedInstanceState: Bundle?) { | ||||||
|         super.onCreate(savedInstanceState) |         super.onCreate(savedInstanceState) | ||||||
|         setContent { |         setContentView(R.layout.fragment_container_main) | ||||||
|             BitcoinSummarizerTheme { |  | ||||||
|                 CryptoScreen() |         // start the root fragment | ||||||
|             } |         App.INSTANCE.router.newRootScreen(Screens.Crypto) | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @SuppressLint("MutableCollectionMutableState", "SimpleDateFormat") |     override fun onResumeFragments() { | ||||||
| @OptIn(ExperimentalMaterial3Api::class) |         super.onResumeFragments() | ||||||
| @Preview |         App.INSTANCE.navigatorHolder.setNavigator(navigator) | ||||||
| @Composable |  | ||||||
| fun CryptoScreen() { |  | ||||||
|     // Создаем репозиторий для работы с API |  | ||||||
|     val cryptoRepository = remember { CryptoRepository(RetrofitClient.apiService) } |  | ||||||
|     // Состояния для хранения результатов запросов |  | ||||||
|     var bitcoinTicker by remember { mutableStateOf(TickerData()) } |  | ||||||
|     var globalData by remember { mutableStateOf(GlobalResponse()) } |  | ||||||
|     var fearGreedDataList by remember { mutableStateOf(ArrayList<FearAndGreedData>()) } |  | ||||||
|  |  | ||||||
|     var fearGreedIndexDaysCount by remember { mutableIntStateOf(30) } |  | ||||||
|  |  | ||||||
|     // Запускаем корутину для выполнения сетевых запросов |  | ||||||
|     LaunchedEffect(fearGreedIndexDaysCount) { |  | ||||||
|         // Запрос Bitcoin Ticker |  | ||||||
|         val tickerResponse = cryptoRepository.fetchBitcoinTicker() |  | ||||||
|         bitcoinTicker = tickerResponse!!.first() |  | ||||||
|  |  | ||||||
|         // Запрос глобальных данных |  | ||||||
|         val globalResponse = cryptoRepository.fetchGlobalData() |  | ||||||
|         globalData = globalResponse!! |  | ||||||
|  |  | ||||||
|         // Запрос индекса страха и жадности |  | ||||||
|         val fearResponse = cryptoRepository.fetchFearAndGreedData(fearGreedIndexDaysCount) |  | ||||||
|         fearGreedDataList = fearResponse?.dataList as ArrayList<FearAndGreedData> |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     val modelProducer = remember { CartesianChartModelProducer() } |     override fun onPause() { | ||||||
|  |         App.INSTANCE.navigatorHolder.removeNavigator() | ||||||
|     LaunchedEffect(fearGreedDataList) { |         super.onPause() | ||||||
|         modelProducer.runTransaction { |  | ||||||
|             var numberValues = Array(fearGreedDataList.count()) { |  | ||||||
|                 index -> |  | ||||||
|                     fearGreedDataList[fearGreedDataList.count() - index - 1] |  | ||||||
|                         .value.toInt() |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if(numberValues.isEmpty()) |  | ||||||
|                 numberValues = Array(1) {0} |  | ||||||
|  |  | ||||||
|             lineSeries { series(numberValues.toList()) } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(rememberTopAppBarState()) |  | ||||||
|     val zoomState = rememberVicoZoomState( |  | ||||||
|         zoomEnabled = false, |  | ||||||
|         initialZoom = Zoom.x(fearGreedIndexDaysCount.toDouble())) |  | ||||||
|  |  | ||||||
|     // Отображаем результаты на странице |  | ||||||
|     Scaffold ( |  | ||||||
|         modifier = Modifier |  | ||||||
|             .nestedScroll(scrollBehavior.nestedScrollConnection), |  | ||||||
|  |  | ||||||
|         topBar = { |  | ||||||
|             MediumTopAppBar( |  | ||||||
|                 colors = TopAppBarDefaults.topAppBarColors( |  | ||||||
|                     containerColor = MaterialTheme.colorScheme.primary, |  | ||||||
|                     scrolledContainerColor = MaterialTheme.colorScheme.primary, |  | ||||||
|                     titleContentColor = MaterialTheme.colorScheme.onPrimary, |  | ||||||
|                     navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, |  | ||||||
|                     actionIconContentColor = MaterialTheme.colorScheme.onPrimary, |  | ||||||
|                 ), |  | ||||||
|                 title = { |  | ||||||
|                     Text( |  | ||||||
|                         "Bitcoin Summarizer", |  | ||||||
|                         maxLines = 1, |  | ||||||
|                         overflow = TextOverflow.Ellipsis |  | ||||||
|                     ) |  | ||||||
|                 }, |  | ||||||
|                 navigationIcon = { |  | ||||||
|                     IconButton(onClick = {}) { |  | ||||||
|                         Icon( |  | ||||||
|                             imageVector = Icons.Filled.Menu, |  | ||||||
|                             contentDescription = "Navigation hamburger menu" |  | ||||||
|                         ) |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 actions = { |  | ||||||
|                     IconButton(onClick = {}) { |  | ||||||
|                         Icon( |  | ||||||
|                             imageVector = Icons.Filled.MoreVert, |  | ||||||
|                             contentDescription = "Actions" |  | ||||||
|                         ) |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 scrollBehavior = scrollBehavior |  | ||||||
|             ) |  | ||||||
|         }, |  | ||||||
|     ) |  | ||||||
|     { innerPadding -> |  | ||||||
|         Box( |  | ||||||
|             modifier = Modifier |  | ||||||
|                 .background(Color.Transparent) |  | ||||||
|                 .padding(innerPadding) |  | ||||||
|                 .verticalScroll(rememberScrollState()) |  | ||||||
|         ) { |  | ||||||
|             Column( |  | ||||||
|                 modifier = Modifier |  | ||||||
|                     .padding(16.dp, 16.dp, 16.dp, 36.dp) |  | ||||||
|             ) { |  | ||||||
|                 /* Fear Greed Chart Start */ |  | ||||||
|                 CartesianChartHost( |  | ||||||
|                     zoomState = zoomState, |  | ||||||
|                     chart = rememberCartesianChart( |  | ||||||
|                         rememberLineCartesianLayer( |  | ||||||
|                             lineProvider = LineCartesianLayer.LineProvider.series( |  | ||||||
|                                 vicoTheme.lineCartesianLayerColors.map{ |  | ||||||
|                                     _ -> |  | ||||||
|                                         LineCartesianLayer |  | ||||||
|                                             .rememberLine( |  | ||||||
|                                                 LineCartesianLayer.LineFill.single( |  | ||||||
|                                                     fill(MaterialTheme.colorScheme.onPrimary) |  | ||||||
|                                                 ) |  | ||||||
|                                             ) |  | ||||||
|                                 } |  | ||||||
|                             ), |  | ||||||
|                             rangeProvider = remember { CartesianLayerRangeProvider.fixed(minY = 0.0, maxY = 100.0)} |  | ||||||
|                         ), |  | ||||||
|                         startAxis = VerticalAxis.rememberStart( |  | ||||||
|                             title = "FGI", |  | ||||||
|                             titleComponent = rememberTextComponent(MaterialTheme.colorScheme.onPrimary), |  | ||||||
|                             line = rememberAxisLineComponent(fill(MaterialTheme.colorScheme.onPrimary)), |  | ||||||
|                             label = rememberAxisLabelComponent(MaterialTheme.colorScheme.onPrimary) |  | ||||||
|                         ), |  | ||||||
|                         bottomAxis = HorizontalAxis.rememberBottom( |  | ||||||
|                             title = "последние $fearGreedIndexDaysCount дней", |  | ||||||
|                             titleComponent = rememberTextComponent(MaterialTheme.colorScheme.onPrimary), |  | ||||||
|                             line = rememberAxisLineComponent(fill(MaterialTheme.colorScheme.onPrimary)), |  | ||||||
|                             label = rememberAxisLabelComponent(MaterialTheme.colorScheme.onPrimary) |  | ||||||
|                         ), |  | ||||||
|                         decorations = listOf( |  | ||||||
|                             remember { |  | ||||||
|                                 HorizontalLine( |  | ||||||
|                                     y = { 25.toDouble() }, |  | ||||||
|                                     line = LineComponent(fill(Flame), 1f), |  | ||||||
|                                     labelComponent = TextComponent( |  | ||||||
|                                         background = |  | ||||||
|                                         shapeComponent( |  | ||||||
|                                             fill(Flame), |  | ||||||
|                                             CorneredShape.rounded( |  | ||||||
|                                                 topLeft = 4.dp, |  | ||||||
|                                                 topRight = 4.dp |  | ||||||
|                                             ) |  | ||||||
|                                         ), |  | ||||||
|                                     ), |  | ||||||
|                                     label = { "Страх" }, |  | ||||||
|                                     verticalLabelPosition = Position.Vertical.Top |  | ||||||
|                                 ) |  | ||||||
|                             }, |  | ||||||
|                             remember { |  | ||||||
|                                 HorizontalLine( |  | ||||||
|                                     y = { 70.toDouble() }, |  | ||||||
|                                     line = LineComponent(fill(Green2), 1f), |  | ||||||
|                                     labelComponent = TextComponent( |  | ||||||
|                                         background = |  | ||||||
|                                         shapeComponent( |  | ||||||
|                                             fill(Green2), |  | ||||||
|                                             CorneredShape.rounded( |  | ||||||
|                                                 bottomLeft = 4.dp, |  | ||||||
|                                                 bottomRight = 4.dp |  | ||||||
|                                             ) |  | ||||||
|                                         ), |  | ||||||
|                                     ), |  | ||||||
|                                     label = { "Жадность" }, |  | ||||||
|                                     verticalLabelPosition = Position.Vertical.Bottom |  | ||||||
|                                 ) |  | ||||||
|                             } |  | ||||||
|                         ), |  | ||||||
|                     ), |  | ||||||
|                     modelProducer = modelProducer, |  | ||||||
|                 ) |  | ||||||
|                 /* Fear Greed Chart End */ |  | ||||||
|                 Spacer(modifier = Modifier.height(16.dp)) |  | ||||||
|                 HorizontalDivider(thickness = 2.dp) |  | ||||||
|                 Spacer(modifier = Modifier.height(16.dp)) |  | ||||||
|                 Text( |  | ||||||
|                     "Данные о Биткоине", |  | ||||||
|                     modifier = Modifier |  | ||||||
|                         .align(alignment = Alignment.CenterHorizontally), |  | ||||||
|                     fontSize = 24.sp |  | ||||||
|                 ) |  | ||||||
|                 Spacer(modifier = Modifier.height(16.dp)) |  | ||||||
|                 val formatter = DecimalFormat("0.00") |  | ||||||
|                 Text("Текущая цена: \n \$ ${formatter.format(bitcoinTicker.priceUsd.toFloat())}\n") |  | ||||||
|                 Text("Суточный оборот: \n \$ ${bitcoinTicker.volume24hUsd}\n") |  | ||||||
|                 Text("Капитализация: \n \$ ${bitcoinTicker.marketCapUsd}\n") |  | ||||||
|                 Text( |  | ||||||
|                     "Изменение курса за: " + |  | ||||||
|                             "\n Сутки: ${formatter.format(bitcoinTicker.percentChange24h.toFloat())}% " + |  | ||||||
|                             "\n Неделю: ${formatter.format(bitcoinTicker.percentChange7d.toFloat())}%\n" |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|                 var humanDate = "" |  | ||||||
|  |  | ||||||
|                 if(bitcoinTicker.lastUpdated.isNotEmpty()) |  | ||||||
|                     humanDate = DateTimeFormatter.ISO_INSTANT |  | ||||||
|                         .format(java.time.Instant.ofEpochSecond(bitcoinTicker.lastUpdated.toLong())) |  | ||||||
|  |  | ||||||
|                 Text("Время последнего обновления: \n ${humanDate}\n") |  | ||||||
|  |  | ||||||
|                 HorizontalDivider(thickness = 2.dp) |  | ||||||
|                 Spacer(modifier = Modifier.height(16.dp)) |  | ||||||
|                 Text( |  | ||||||
|                     "Глобальные данные", |  | ||||||
|                     modifier = Modifier |  | ||||||
|                         .align(alignment = Alignment.CenterHorizontally), |  | ||||||
|                     fontSize = 24.sp |  | ||||||
|                 ) |  | ||||||
|                 Spacer(modifier = Modifier.height(16.dp)) |  | ||||||
|                 Text("Общая капитализация крипторынка: \n \$ ${globalData.totalMarketCapUsd}\n") |  | ||||||
|                 Text("Всего тикеров: \n ${globalData.activeCryptocurrencies}\n") |  | ||||||
|                 Text("Суточный оборот всех криптовалют: \n \$ ${globalData.total24hVolumeUsd}\n") |  | ||||||
|                 Text("Процент доминации Биткоина: \n ${formatter.format(globalData.bitcoinPercentageOfMarketCap.toFloat())}%\n") |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,72 @@ | |||||||
|  | package ru.vendetti.bitcoin_summarizer | ||||||
|  |  | ||||||
|  | import androidx.compose.foundation.layout.padding | ||||||
|  | import androidx.compose.material3.HorizontalDivider | ||||||
|  | import androidx.compose.material3.MaterialTheme | ||||||
|  | import androidx.compose.material3.ModalDrawerSheet | ||||||
|  | import androidx.compose.material3.NavigationDrawerItem | ||||||
|  | import androidx.compose.material3.NavigationDrawerItemColors | ||||||
|  | import androidx.compose.material3.NavigationDrawerItemDefaults | ||||||
|  | import androidx.compose.material3.Text | ||||||
|  | import androidx.compose.runtime.Composable | ||||||
|  | import androidx.compose.ui.Modifier | ||||||
|  | import androidx.compose.ui.unit.dp | ||||||
|  | import androidx.compose.ui.unit.sp | ||||||
|  | import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme | ||||||
|  |  | ||||||
|  | @Composable | ||||||
|  | fun MyHamburgerModal( | ||||||
|  |     screenKey: ScreenKey | ||||||
|  | ) { | ||||||
|  |     ModalDrawerSheet( | ||||||
|  |         drawerContainerColor = MaterialTheme.colorScheme.primary, | ||||||
|  |         drawerContentColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |     ) { | ||||||
|  |         Text("Bitcoin summarizer", | ||||||
|  |             modifier = Modifier.padding(16.dp), | ||||||
|  |             fontSize = 24.sp) | ||||||
|  |         HorizontalDivider() | ||||||
|  |         NavigationDrawerItem( | ||||||
|  |             colors = NavigationDrawerItemDefaults.colors( | ||||||
|  |                 selectedContainerColor = MaterialTheme.colorScheme.tertiary, | ||||||
|  |                 selectedTextColor = MaterialTheme.colorScheme.onTertiary, | ||||||
|  |                 unselectedTextColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |             ), | ||||||
|  |             label = { Text("Crypto statistics") }, | ||||||
|  |             selected = (screenKey == ScreenKey.Crypto), | ||||||
|  |             onClick = { | ||||||
|  |                 if (screenKey != ScreenKey.Crypto) { | ||||||
|  |                     App.INSTANCE.router.replaceScreen(Screens.Crypto) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         NavigationDrawerItem( | ||||||
|  |             colors = NavigationDrawerItemDefaults.colors( | ||||||
|  |                 selectedContainerColor = MaterialTheme.colorScheme.tertiary, | ||||||
|  |                 selectedTextColor = MaterialTheme.colorScheme.onTertiary, | ||||||
|  |                 unselectedTextColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |             ), | ||||||
|  |             label = { Text("About") }, | ||||||
|  |             selected = (screenKey == ScreenKey.About), | ||||||
|  |             onClick = { | ||||||
|  |                 if (screenKey != ScreenKey.About) { | ||||||
|  |                     App.INSTANCE.router.replaceScreen(Screens.About) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         NavigationDrawerItem( | ||||||
|  |             colors = NavigationDrawerItemDefaults.colors( | ||||||
|  |                 selectedContainerColor = MaterialTheme.colorScheme.tertiary, | ||||||
|  |                 selectedTextColor = MaterialTheme.colorScheme.onTertiary, | ||||||
|  |                 unselectedTextColor = MaterialTheme.colorScheme.onPrimary, | ||||||
|  |             ), | ||||||
|  |             label = { Text("Learn") }, | ||||||
|  |             selected = (screenKey == ScreenKey.Learn), | ||||||
|  |             onClick = { | ||||||
|  |                 if (screenKey != ScreenKey.Learn) { | ||||||
|  |                     App.INSTANCE.router.replaceScreen(Screens.Learn) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										23
									
								
								app/src/main/java/ru/vendetti/bitcoin_summarizer/Screens.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								app/src/main/java/ru/vendetti/bitcoin_summarizer/Screens.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | package ru.vendetti.bitcoin_summarizer | ||||||
|  |  | ||||||
|  | import androidx.fragment.app.Fragment | ||||||
|  | import androidx.fragment.app.FragmentFactory | ||||||
|  | import com.github.terrakok.cicerone.androidx.FragmentScreen | ||||||
|  |  | ||||||
|  | class Screens { | ||||||
|  |     object Crypto: FragmentScreen { | ||||||
|  |         override fun createFragment(factory: FragmentFactory) = CryptoFragment() | ||||||
|  |     } | ||||||
|  |     object About: FragmentScreen { | ||||||
|  |         override fun createFragment(factory: FragmentFactory) = AboutFragment() | ||||||
|  |     } | ||||||
|  |     object Learn: FragmentScreen { | ||||||
|  |         override fun createFragment(factory: FragmentFactory) = LearnFragment() | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | enum class ScreenKey { | ||||||
|  |     Crypto, | ||||||
|  |     About, | ||||||
|  |     Learn | ||||||
|  | } | ||||||
| @@ -15,14 +15,14 @@ import androidx.compose.ui.platform.LocalContext | |||||||
| private val DarkColorScheme = darkColorScheme( | private val DarkColorScheme = darkColorScheme( | ||||||
|     primary = RaisinBlack, |     primary = RaisinBlack, | ||||||
|     secondary = DarkPurple, |     secondary = DarkPurple, | ||||||
|     tertiary = EnglishViolet, |     tertiary = HunyadiYellow, | ||||||
|  |  | ||||||
|     // Other default colors to override |     // Other default colors to override | ||||||
|     background = DarkPurple, |     background = DarkPurple, | ||||||
|     surface = Color(0xFFFFFBFE), |     surface = Color(0xFFFFFBFE), | ||||||
|     onPrimary = HunyadiYellow, |     onPrimary = HunyadiYellow, | ||||||
|     onSecondary = HunyadiYellow, |     onSecondary = HunyadiYellow, | ||||||
|     onTertiary = HunyadiYellow, |     onTertiary = DarkPurple, | ||||||
|     onBackground = HunyadiYellow, |     onBackground = HunyadiYellow, | ||||||
|     onSurface = Color(0xFF1C1B1F), |     onSurface = Color(0xFF1C1B1F), | ||||||
| ) | ) | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								app/src/main/res/layout/fragment_compose_view.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/layout/fragment_compose_view.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:orientation="vertical" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |     <androidx.compose.ui.platform.ComposeView | ||||||
|  |         android:id="@+id/compose_view" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" /> | ||||||
|  | </LinearLayout> | ||||||
							
								
								
									
										10
									
								
								app/src/main/res/layout/fragment_container_main.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/layout/fragment_container_main.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:orientation="vertical" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent"> | ||||||
|  |     <FrameLayout | ||||||
|  |         android:id="@+id/container" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent"/> | ||||||
|  | </LinearLayout> | ||||||
							
								
								
									
										4
									
								
								app/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								app/src/main/res/values/ids.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <resources> | ||||||
|  |     <item name="container" type="id" /> | ||||||
|  | </resources> | ||||||
| @@ -13,6 +13,8 @@ activityCompose = "1.10.1" | |||||||
| composeBom = "2025.02.00" | composeBom = "2025.02.00" | ||||||
| retrofit = "2.9.0" | retrofit = "2.9.0" | ||||||
| vico = "2.0.2" | vico = "2.0.2" | ||||||
|  | ciceroneVer = "7.1" | ||||||
|  | fragmentKtx = "1.8.6" | ||||||
|  |  | ||||||
| [libraries] | [libraries] | ||||||
| androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } | ||||||
| @@ -34,6 +36,8 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine | |||||||
| kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutinesCore" } | ||||||
| retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } | retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } | ||||||
| vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" } | vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" } | ||||||
|  | cicerone = {module = "com.github.terrakok:cicerone", version.ref = "ciceroneVer"} | ||||||
|  | androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } | ||||||
|  |  | ||||||
| [plugins] | [plugins] | ||||||
| android-application = { id = "com.android.application", version.ref = "agp" } | android-application = { id = "com.android.application", version.ref = "agp" } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user