From 70c6eb0834012c06d1f23fab5d9db7bd72424674 Mon Sep 17 00:00:00 2001 From: b3s23 Date: Mon, 10 Mar 2025 21:51:16 +0500 Subject: [PATCH] Fixed some design issues; Changed chart's horizontal axis so that it displays correct date for each value of fear greed index; Implemented Cicerone as navigation library for the app; Implemented two additional screens - About and Learn; Implemented navigation drawer and made it open the respected screens as well as highlighting current screen in the list; TODO: About and Learn screens (text) --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 1 + .../bitcoin_summarizer/AboutFragment.kt | 119 +++++ .../ru/vendetti/bitcoin_summarizer/App.kt | 23 + .../bitcoin_summarizer/CryptoFragment.kt | 411 ++++++++++++++++++ .../bitcoin_summarizer/LearnFragment.kt | 119 +++++ .../bitcoin_summarizer/MainActivity.kt | 248 +---------- .../bitcoin_summarizer/MyHamburgerModal.kt | 72 +++ .../ru/vendetti/bitcoin_summarizer/Screens.kt | 23 + .../bitcoin_summarizer/ui/theme/Theme.kt | 4 +- .../main/res/layout/fragment_compose_view.xml | 10 + .../res/layout/fragment_container_main.xml | 10 + app/src/main/res/values/ids.xml | 4 + gradle/libs.versions.toml | 4 + 14 files changed, 821 insertions(+), 229 deletions(-) create mode 100644 app/src/main/java/ru/vendetti/bitcoin_summarizer/AboutFragment.kt create mode 100644 app/src/main/java/ru/vendetti/bitcoin_summarizer/App.kt create mode 100644 app/src/main/java/ru/vendetti/bitcoin_summarizer/CryptoFragment.kt create mode 100644 app/src/main/java/ru/vendetti/bitcoin_summarizer/LearnFragment.kt create mode 100644 app/src/main/java/ru/vendetti/bitcoin_summarizer/MyHamburgerModal.kt create mode 100644 app/src/main/java/ru/vendetti/bitcoin_summarizer/Screens.kt create mode 100644 app/src/main/res/layout/fragment_compose_view.xml create mode 100644 app/src/main/res/layout/fragment_container_main.xml create mode 100644 app/src/main/res/values/ids.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6558deb..14c6109 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.fragment.ktx) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) @@ -62,5 +63,6 @@ dependencies { implementation(libs.kotlinx.coroutines.android) implementation(libs.kotlinx.coroutines.core) implementation(libs.vico.compose.m3) + implementation(libs.cicerone) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dc24fe6..0498372 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> (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()) + ) { + + } + } + } +} diff --git a/app/src/main/java/ru/vendetti/bitcoin_summarizer/App.kt b/app/src/main/java/ru/vendetti/bitcoin_summarizer/App.kt new file mode 100644 index 0000000..6f62e95 --- /dev/null +++ b/app/src/main/java/ru/vendetti/bitcoin_summarizer/App.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vendetti/bitcoin_summarizer/CryptoFragment.kt b/app/src/main/java/ru/vendetti/bitcoin_summarizer/CryptoFragment.kt new file mode 100644 index 0000000..8246cfb --- /dev/null +++ b/app/src/main/java/ru/vendetti/bitcoin_summarizer/CryptoFragment.kt @@ -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(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()) } + + 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 + 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() + } + } + + 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("Загрузка...") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vendetti/bitcoin_summarizer/LearnFragment.kt b/app/src/main/java/ru/vendetti/bitcoin_summarizer/LearnFragment.kt new file mode 100644 index 0000000..3c0d641 --- /dev/null +++ b/app/src/main/java/ru/vendetti/bitcoin_summarizer/LearnFragment.kt @@ -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(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()) + ) { + + } + } + } +} diff --git a/app/src/main/java/ru/vendetti/bitcoin_summarizer/MainActivity.kt b/app/src/main/java/ru/vendetti/bitcoin_summarizer/MainActivity.kt index 0d020b4..df39b3b 100644 --- a/app/src/main/java/ru/vendetti/bitcoin_summarizer/MainActivity.kt +++ b/app/src/main/java/ru/vendetti/bitcoin_summarizer/MainActivity.kt @@ -2,6 +2,7 @@ package ru.vendetti.bitcoin_summarizer import android.annotation.SuppressLint import android.os.Bundle +import android.widget.FrameLayout import androidx.activity.ComponentActivity import androidx.activity.compose.setContent 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.unit.dp 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.axis.rememberAxisLabelComponent 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.time.format.DateTimeFormatter -class MainActivity : ComponentActivity() { +class MainActivity : FragmentActivity() { + + private val navigator = AppNavigator(this, R.id.container) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContent { - BitcoinSummarizerTheme { - CryptoScreen() - } - } - } -} - -@SuppressLint("MutableCollectionMutableState", "SimpleDateFormat") -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@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()) } - - 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 - } - - val modelProducer = remember { CartesianChartModelProducer() } - - LaunchedEffect(fearGreedDataList) { - 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") - } - } + setContentView(R.layout.fragment_container_main) + + // start the root fragment + App.INSTANCE.router.newRootScreen(Screens.Crypto) + } + + override fun onResumeFragments() { + super.onResumeFragments() + App.INSTANCE.navigatorHolder.setNavigator(navigator) + } + + override fun onPause() { + App.INSTANCE.navigatorHolder.removeNavigator() + super.onPause() } } diff --git a/app/src/main/java/ru/vendetti/bitcoin_summarizer/MyHamburgerModal.kt b/app/src/main/java/ru/vendetti/bitcoin_summarizer/MyHamburgerModal.kt new file mode 100644 index 0000000..40b5954 --- /dev/null +++ b/app/src/main/java/ru/vendetti/bitcoin_summarizer/MyHamburgerModal.kt @@ -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) + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/vendetti/bitcoin_summarizer/Screens.kt b/app/src/main/java/ru/vendetti/bitcoin_summarizer/Screens.kt new file mode 100644 index 0000000..8c0cec7 --- /dev/null +++ b/app/src/main/java/ru/vendetti/bitcoin_summarizer/Screens.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/ru/vendetti/bitcoin_summarizer/ui/theme/Theme.kt b/app/src/main/java/ru/vendetti/bitcoin_summarizer/ui/theme/Theme.kt index cf97fbd..1502de0 100644 --- a/app/src/main/java/ru/vendetti/bitcoin_summarizer/ui/theme/Theme.kt +++ b/app/src/main/java/ru/vendetti/bitcoin_summarizer/ui/theme/Theme.kt @@ -15,14 +15,14 @@ import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( primary = RaisinBlack, secondary = DarkPurple, - tertiary = EnglishViolet, + tertiary = HunyadiYellow, // Other default colors to override background = DarkPurple, surface = Color(0xFFFFFBFE), onPrimary = HunyadiYellow, onSecondary = HunyadiYellow, - onTertiary = HunyadiYellow, + onTertiary = DarkPurple, onBackground = HunyadiYellow, onSurface = Color(0xFF1C1B1F), ) diff --git a/app/src/main/res/layout/fragment_compose_view.xml b/app/src/main/res/layout/fragment_compose_view.xml new file mode 100644 index 0000000..ab774c5 --- /dev/null +++ b/app/src/main/res/layout/fragment_compose_view.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_container_main.xml b/app/src/main/res/layout/fragment_container_main.xml new file mode 100644 index 0000000..d6cc091 --- /dev/null +++ b/app/src/main/res/layout/fragment_container_main.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..f76ac19 --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cdc58bf..59ea3a6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,8 @@ activityCompose = "1.10.1" composeBom = "2025.02.00" retrofit = "2.9.0" vico = "2.0.2" +ciceroneVer = "7.1" +fragmentKtx = "1.8.6" [libraries] 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" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } 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] android-application = { id = "com.android.application", version.ref = "agp" }