2 Commits
1.3 ... dev

Author SHA1 Message Date
ed305ffeba Added saving data to internal storage for later use.
Added dialog on internal storage usage, and for first start without internet connection
2025-03-12 17:30:23 +05:00
30102eb8aa Implemented About screen with links and email addresses 2025-03-11 00:59:38 +05:00
9 changed files with 267 additions and 33 deletions

View File

@ -1,3 +1,4 @@
plugins { plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
@ -64,5 +65,6 @@ dependencies {
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.vico.compose.m3) implementation(libs.vico.compose.m3)
implementation(libs.cicerone) implementation(libs.cicerone)
implementation(libs.androidx.datastore)
} }

View File

@ -4,10 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box 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.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
@ -15,6 +21,7 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -27,15 +34,30 @@ import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
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.sp
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme
import ru.vendetti.bitcoin_summarizer.ui.theme.Flame
class AboutFragment: Fragment() { class AboutFragment: Fragment() {
override fun onCreateView( override fun onCreateView(
@ -55,7 +77,7 @@ class AboutFragment: Fragment() {
} }
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class)
@Composable @Composable
@Preview @Preview
fun AboutComposable() { fun AboutComposable() {
@ -112,7 +134,90 @@ fun AboutComposable() {
.padding(innerPadding) .padding(innerPadding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Column(
modifier = Modifier
.padding(16.dp, 16.dp, 16.dp, 36.dp)
) {
Icon(
painter = painterResource(id = R.mipmap.ic_launcher_foreground),
contentDescription = null,
modifier = Modifier
.width(160.dp)
.height(160.dp)
.align(Alignment.CenterHorizontally)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
"Репозиторий проекта",
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally),
fontSize = 24.sp
)
Spacer(modifier = Modifier.height(16.dp))
val repositoryLinkString = buildAnnotatedString {
withLink(LinkAnnotation.Url(
url = "https://git.vendetti.ru/andy/bitcoin-summarizer/tags",
styles = TextLinkStyles(style = SpanStyle(color = Flame))
)
) {
append("Ссылка на репозиторий")
}
}
Text(text = repositoryLinkString)
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(16.dp))
Text(
"Авторы",
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally),
fontSize = 24.sp
)
Spacer(modifier = Modifier.height(16.dp))
Text("Студенты гр. 9ИСП-42-21, ГБПОУ УКРТБ")
Spacer(modifier = Modifier.height(16.dp))
Text("Васильев Андрей Дмитриевич - Back-end: CI/CD (Continuous Integration), Api interactions, Database interactions")
Spacer(modifier = Modifier.height(8.dp))
SelectionContainer {
Text("andy@vendetti.ru", color = Flame)
}
Spacer(modifier = Modifier.height(16.dp))
Text("Зубарев Артемий Альбертович - Front-end: Charts, App navigation (Cicerone)")
Spacer(modifier = Modifier.height(8.dp))
SelectionContainer {
Text("artemiy.work32@gmail.com", color = Flame)
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(16.dp))
Text(
"Используемый API",
modifier = Modifier
.align(alignment = Alignment.CenterHorizontally),
fontSize = 24.sp
)
val apiLinkString = buildAnnotatedString {
withLink(LinkAnnotation.Url(
url = "https://alternative.me/crypto/api/",
styles = TextLinkStyles(style = SpanStyle(color = Flame))
)
) {
append("Ссылка на API")
}
}
Text(text = apiLinkString)
}
} }
} }
} }

View File

@ -1,21 +1,33 @@
package ru.vendetti.bitcoin_summarizer package ru.vendetti.bitcoin_summarizer
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DrawerValue import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -23,11 +35,10 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
@ -42,13 +53,19 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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.compose.ui.window.Dialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.gson.Gson
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
@ -80,6 +97,9 @@ import kotlinx.coroutines.launch
import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme
import ru.vendetti.bitcoin_summarizer.ui.theme.Flame import ru.vendetti.bitcoin_summarizer.ui.theme.Flame
import ru.vendetti.bitcoin_summarizer.ui.theme.Green2 import ru.vendetti.bitcoin_summarizer.ui.theme.Green2
import java.io.FileInputStream
import java.io.FileOutputStream
import java.lang.StringBuilder
import java.text.DecimalFormat import java.text.DecimalFormat
import java.time.Instant import java.time.Instant
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -102,11 +122,51 @@ class CryptoFragment: Fragment() {
} }
} }
@Composable
fun ShowAlertDialog(
onDismissRequest: () -> Unit,
onConfirmation: () -> Unit,
dialogTitle: String,
dialogText: String,
) {
AlertDialog(
icon = {},
title = {
Text(text = dialogTitle)
},
text = {
Text(text = dialogText)
},
onDismissRequest = {
onDismissRequest()
},
confirmButton = {},
dismissButton = {
TextButton(
onClick = {
onDismissRequest()
}
) {
Text(
text = "Окей",
color = MaterialTheme.colorScheme.onPrimary
)
}
},
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary,
textContentColor = MaterialTheme.colorScheme.onPrimary,
)
}
@SuppressLint("MutableCollectionMutableState", "SimpleDateFormat") @SuppressLint("MutableCollectionMutableState", "SimpleDateFormat")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Preview @Preview
@Composable @Composable
fun CryptoComposable() { fun CryptoComposable() {
val openAlertDialog1 = remember { mutableStateOf(false) }
val openAlertDialog2 = remember { mutableStateOf(false) }
// Создаем репозиторий для работы с API // Создаем репозиторий для работы с API
val cryptoRepository = remember { CryptoRepository(RetrofitClient.apiService) } val cryptoRepository = remember { CryptoRepository(RetrofitClient.apiService) }
// Состояния для хранения результатов запросов // Состояния для хранения результатов запросов
@ -116,8 +176,36 @@ fun CryptoComposable() {
var fearGreedIndexDaysCount by remember { mutableIntStateOf(30) } var fearGreedIndexDaysCount by remember { mutableIntStateOf(30) }
val gson = Gson()
val context = LocalContext.current
when {
openAlertDialog1.value -> {
ShowAlertDialog(
onDismissRequest = {openAlertDialog1.value = false},
onConfirmation = {},
dialogTitle = "Внимание!",
dialogText = "Отсутствует интернет-подключение, загружаю сохраненные данные. " +
"" +
"Обратите внимание, что данные могут быть неактуальными.",
)
}
openAlertDialog2.value -> {
ShowAlertDialog(
onDismissRequest = {
openAlertDialog2.value = false
App.INSTANCE.router.exit()
},
onConfirmation = {},
dialogTitle = "Внимание!",
dialogText = "При первом подключении необходимо подключение к интернету!",
)
}
}
// Запускаем корутину для выполнения сетевых запросов // Запускаем корутину для выполнения сетевых запросов
LaunchedEffect(fearGreedIndexDaysCount) { LaunchedEffect(fearGreedIndexDaysCount, context) {
try { try {
// Запрос Bitcoin Ticker // Запрос Bitcoin Ticker
val tickerResponse = cryptoRepository.fetchBitcoinTicker() val tickerResponse = cryptoRepository.fetchBitcoinTicker()
@ -131,31 +219,53 @@ fun CryptoComposable() {
val fearResponse = cryptoRepository.fetchFearAndGreedData(fearGreedIndexDaysCount) val fearResponse = cryptoRepository.fetchFearAndGreedData(fearGreedIndexDaysCount)
fearGreedDataList = fearResponse?.dataList as ArrayList<FearAndGreedData> fearGreedDataList = fearResponse?.dataList as ArrayList<FearAndGreedData>
fearGreedDataList.reverse() fearGreedDataList.reverse()
}catch (e: Exception) {
bitcoinTicker = TickerData( // *** Сохраняем на диск ***
id = "", // 1) Сбор данных в один класс
name = "", val fullStatistics = StatisticsFull(
symbol = "", FGI_list = fearGreedDataList,
rank = "", TickerData = bitcoinTicker,
priceUsd = "", GlobalData = globalData,
priceBtc = "",
volume24hUsd = "",
marketCapUsd = "",
availableSupply = "",
totalSupply = "",
maxSupply = "",
percentChange1h = "",
percentChange24h = "",
percentChange7d = "",
lastUpdated = ""
) )
globalData = GlobalResponse(
activeCryptocurrencies = "", // 2) Перевод класса в json-строку
totalMarketCapUsd = "", val statisticsJsonString = gson.toJson(fullStatistics)
total24hVolumeUsd = "",
bitcoinPercentageOfMarketCap = "" // 3) Сохранение в файл во внутреннем хранилище
) val fos: FileOutputStream =
fearGreedDataList = ArrayList<FearAndGreedData>() context.openFileOutput("statistics.txt", Context.MODE_PRIVATE)
fos.write(statisticsJsonString.toByteArray())
fos.flush()
fos.close()
}
catch (e: Exception) {
try {
// *** Загрузка данных из файла ***
// 1) Получение строки из файла
val fis: FileInputStream =
context.openFileInput("statistics.txt")
var a: Int
val temp = StringBuilder()
while (fis.read().also { a = it } != -1) {
temp.append(a.toChar())
}
// Показ предупреждения.
openAlertDialog1.value = true
val statisticsJsonString = temp.toString()
// 2) Перевод строки в класс
val statisticsFull = gson.fromJson(statisticsJsonString, StatisticsFull::class.java)
// 3) Распределение данных по переменным
bitcoinTicker = statisticsFull.TickerData
globalData = statisticsFull.GlobalData
fearGreedDataList = statisticsFull.FGI_list
}
catch (e: Exception) {
openAlertDialog2.value = true
}
} }
} }

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@ -33,6 +34,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.style.TextOverflow 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.fragment.app.Fragment import androidx.fragment.app.Fragment
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme import ru.vendetti.bitcoin_summarizer.ui.theme.BitcoinSummarizerTheme
@ -112,8 +114,13 @@ fun LearnComposable() {
.padding(innerPadding) .padding(innerPadding)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
Column (
modifier = Modifier
.padding(16.dp, 16.dp, 16.dp, 36.dp)
) {
} }
} }
} }
} }
}

View File

@ -54,7 +54,7 @@ fun MyHamburgerModal(
} }
} }
) )
NavigationDrawerItem( /*NavigationDrawerItem(
colors = NavigationDrawerItemDefaults.colors( colors = NavigationDrawerItemDefaults.colors(
selectedContainerColor = MaterialTheme.colorScheme.tertiary, selectedContainerColor = MaterialTheme.colorScheme.tertiary,
selectedTextColor = MaterialTheme.colorScheme.onTertiary, selectedTextColor = MaterialTheme.colorScheme.onTertiary,
@ -67,6 +67,6 @@ fun MyHamburgerModal(
App.INSTANCE.router.replaceScreen(Screens.Learn) App.INSTANCE.router.replaceScreen(Screens.Learn)
} }
} }
) )*/
} }
} }

View File

@ -0,0 +1,7 @@
package ru.vendetti.bitcoin_summarizer
class StatisticsFull (
val FGI_list: ArrayList<FearAndGreedData>,
val TickerData: TickerData,
val GlobalData: GlobalResponse,
)

View File

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

View File

@ -7,4 +7,5 @@
<color name="teal_700">#FF018786</color> <color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color> <color name="white">#FFFFFFFF</color>
<color name="black2">#221E22</color>
</resources> </resources>

View File

@ -15,6 +15,7 @@ retrofit = "2.9.0"
vico = "2.0.2" vico = "2.0.2"
ciceroneVer = "7.1" ciceroneVer = "7.1"
fragmentKtx = "1.8.6" fragmentKtx = "1.8.6"
protoDataStore = "1.1.3"
[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" }
@ -38,6 +39,7 @@ 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"} cicerone = {module = "com.github.terrakok:cicerone", version.ref = "ciceroneVer"}
androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" } androidx-fragment-ktx = { group = "androidx.fragment", name = "fragment-ktx", version.ref = "fragmentKtx" }
androidx-datastore = {group = "androidx.datastore", name = "datastore", version.ref = "protoDataStore"}
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }