dev #26

Merged
B3S23 merged 2 commits from dev into main 2025-03-10 19:52:47 +03:00
14 changed files with 821 additions and 229 deletions

View File

@ -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)
} }

View File

@ -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"

View File

@ -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())
) {
}
}
}
}

View 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
}
}

View File

@ -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("Загрузка...")
}
}
}
}
}
}

View File

@ -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())
) {
}
}
}
}

View File

@ -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)
} }
}
} override fun onResumeFragments() {
super.onResumeFragments()
@SuppressLint("MutableCollectionMutableState", "SimpleDateFormat") App.INSTANCE.navigatorHolder.setNavigator(navigator)
@OptIn(ExperimentalMaterial3Api::class) }
@Preview
@Composable override fun onPause() {
fun CryptoScreen() { App.INSTANCE.navigatorHolder.removeNavigator()
// Создаем репозиторий для работы с API super.onPause()
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() }
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")
}
}
} }
} }

View File

@ -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)
}
}
)
}
}

View 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
}

View File

@ -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),
) )

View 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>

View 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>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="container" type="id" />
</resources>

View File

@ -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" }