61 Commits

Author SHA1 Message Date
289472d4c9 Merge pull request 'dev' (#26) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 3m29s
Reviewed-on: #26
2025-03-10 19:52:47 +03:00
70c6eb0834 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)
2025-03-10 21:51:16 +05:00
fc1cdfea0a Merge pull request 'main' (#24) from main into dev
Reviewed-on: #24
2025-03-08 22:13:53 +03:00
4edf0f7352 Merge pull request 'Some minor fixes' (#23) from ops into main
Reviewed-on: #23
2025-03-08 22:11:34 +03:00
5529b3dafc Some minor fixes 2025-03-09 00:08:36 +05:00
087b732fa5 Merge pull request 'dev' (#22) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 3m32s
Reviewed-on: #22
2025-03-08 15:50:58 +03:00
08df502b1f Some minor fixes (manifest, text typos, gradle app ver etc.) 2025-03-08 17:48:53 +05:00
28196510a3 .idea dir removed from the repo 2025-03-08 17:37:03 +05:00
3691477e92 dependencies updated 2025-03-08 17:28:12 +05:00
580c5b6611 Merge pull request 'removed dynamic color, cause we are controlling colors by ourself' (#21) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m36s
Reviewed-on: #21
2025-03-08 11:45:03 +03:00
8f0ffb7041 removed dynamic color, cause we are controlling colors by ourself 2025-03-08 11:44:29 +03:00
0494efc2ad Merge pull request 'Fixed the design, added full color schemes for light and dark themes. Fixed number formatting with bitcoin data, as well as the line chart being fully visible on the vertical axis now! :3' (#20) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m27s
Reviewed-on: #20
2025-03-07 16:13:29 +03:00
09871e8727 Fixed the design, added full color schemes for light and dark themes. Fixed number formatting with bitcoin data, as well as the line chart being fully visible on the vertical axis now! :3 2025-03-07 18:10:43 +05:00
d220a03a89 Merge pull request 'Update .gitignore' (#19) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m18s
Reviewed-on: #19
2025-03-06 17:40:05 +03:00
95b2c1e70b Merge pull request 'CI/CD branching fix' (#18) from pipeline-test into main
Reviewed-on: #18
2025-03-05 19:42:02 +03:00
4005a39696 CI/CD branching fix
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m18s
2025-03-05 21:40:07 +05:00
407a5d32a5 Merge pull request 'pipeline-test' (#17) from pipeline-test into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m18s
Reviewed-on: #17
2025-03-05 19:34:40 +03:00
81ab379e0e CI/CD branching update
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m25s
2025-03-05 21:30:26 +05:00
5a4d30b9f8 CI/CD image update revert
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m17s
2025-03-05 21:18:04 +05:00
5c92f74ceb CI/CD image update fix
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m23s
2025-03-05 21:14:13 +05:00
0906b95273 CI/CD image update
Some checks failed
Gitea Android Builder / Build (push) Failing after 20s
2025-03-05 21:11:15 +05:00
4c9c7a3aa4 Update .gitignore 2025-03-05 15:53:47 +03:00
07ad6cb59d Merge pull request 'dev' (#16) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m56s
Reviewed-on: #16
2025-03-05 03:51:27 +03:00
fcd27175ac Update app/src/main/java/ru/vendetti/bitcoin_summarizer/TickerResponse.kt 2025-03-05 03:50:25 +03:00
a9c5282bdc Update app/src/main/java/ru/vendetti/bitcoin_summarizer/GlobalResponse.kt 2025-03-05 03:50:08 +03:00
52386c1987 Update app/src/main/java/ru/vendetti/bitcoin_summarizer/MainActivity.kt 2025-03-05 03:49:25 +03:00
fb59407f8b Update gradle/libs.versions.toml 2025-03-05 03:48:52 +03:00
1261b80346 Update app/build.gradle.kts 2025-03-05 03:48:26 +03:00
a1b7569ef0 Merge pull request 'pipeline-test' (#15) from pipeline-test into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 59s
Reviewed-on: #15
2025-03-04 22:06:06 +03:00
d1bc545a07 CI/CD time optimization attempt 6
All checks were successful
Gitea Android Builder / Build (push) Successful in 55s
2025-03-04 23:44:32 +05:00
9c184c08a8 CI/CD time optimization attempt 6
Some checks failed
Gitea Android Builder / Build (push) Failing after 54s
2025-03-04 23:40:39 +05:00
186f1e693c CI/CD time optimization attempt 5
Some checks failed
Gitea Android Builder / Build (push) Failing after 46s
2025-03-04 23:36:03 +05:00
3e6e44c4b3 CI/CD time optimization attempt 5
Some checks failed
Gitea Android Builder / Build (push) Failing after 58s
2025-03-04 23:32:49 +05:00
da80ba3abd CI/CD time optimization attempt 5
All checks were successful
Gitea Android Builder / Build (push) Successful in 57s
2025-03-04 23:27:17 +05:00
f05f4fbbad CI/CD time optimization attempt 4
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m3s
2025-03-04 23:18:04 +05:00
91a8b0380b CI/CD time optimization attempt 3
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m8s
2025-03-04 23:15:46 +05:00
3b196de1d8 CI/CD time optimization attempt 2
All checks were successful
Gitea Android Builder / Build (push) Successful in 55s
2025-03-04 22:43:32 +05:00
b4beaf4f94 CI/CD time optimization attempt
All checks were successful
Gitea Android Builder / Build (push) Successful in 2m58s
2025-03-04 22:37:45 +05:00
85c9bd50cd Merge pull request 'MVP' (#14) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m9s
Reviewed-on: #14
2025-03-03 22:21:51 +03:00
e6e9d7e082 MVP 2025-03-04 00:05:15 +05:00
d3d7bd391b Merge pull request 'Dependencies updated and example screen added' (#13) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 2m20s
Reviewed-on: #13
2025-03-03 19:52:52 +03:00
3142cd79f6 Dependencies updated and example screen added 2025-03-03 21:51:02 +05:00
e6d8a5b7aa Merge pull request 'Data classes and other retrofit logic added' (#12) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 2m20s
Reviewed-on: #12
2025-03-03 19:28:18 +03:00
821162964b Data classes and other retrofit logic added 2025-03-03 21:25:17 +05:00
fa0905486a Merge pull request 'CI/CD revamp fix' (#11) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m4s
Reviewed-on: #11
2025-02-27 20:36:18 +03:00
c0d5908a6e CI/CD revamp fix 2025-02-27 22:36:01 +05:00
d0598853bd Merge pull request 'CI/CD revamp fix' (#10) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 1m15s
Reviewed-on: #10
2025-02-27 20:34:09 +03:00
3a62d99c40 CI/CD revamp fix 2025-02-27 22:33:30 +05:00
9dfdd328db Merge pull request 'CI/CD revamp fix' (#9) from dev into main
All checks were successful
Gitea Android Builder / Build (push) Successful in 3m52s
Reviewed-on: #9
2025-02-27 20:06:45 +03:00
286109dc38 CI/CD revamp fix 2025-02-27 22:03:56 +05:00
fd77fbd97e Merge pull request 'CI/CD revamp fix' (#8) from dev into main
Some checks failed
Gitea Android Builder / Build (push) Failing after 5m38s
Reviewed-on: #8
2025-02-27 19:55:30 +03:00
69d8ae9d7a CI/CD revamp fix 2025-02-27 21:55:05 +05:00
08bdb6f481 Merge pull request 'CI/CD revamp fix' (#7) from dev into main
Some checks failed
Gitea Android Builder / Build (push) Failing after 1m50s
Reviewed-on: #7
2025-02-27 19:48:13 +03:00
fc9f054dec CI/CD revamp fix 2025-02-27 21:47:46 +05:00
f33906b965 Merge pull request 'dev' (#6) from dev into main
Some checks failed
Gitea Android Builder / Build (push) Failing after 29s
Reviewed-on: #6
2025-02-27 19:45:38 +03:00
be208912b7 CI/CD revamp 2025-02-27 21:44:48 +05:00
2f7a94b5de Merge pull request 'main' (#5) from main into dev
Reviewed-on: #5
2025-02-27 18:37:43 +03:00
b42487feba revert 9f0d1de386
revert Update README.md
2025-02-27 18:36:34 +03:00
9f0d1de386 Update README.md 2025-02-27 18:31:20 +03:00
a77610c285 Merge pull request 'Update Readme.md' (#3) from b3s23-patch-1 into main
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 30s
Reviewed-on: #3
2025-02-27 18:24:30 +03:00
0c182b0c63 Update Readme.md
:3
2025-02-27 18:23:37 +03:00
60 changed files with 1190 additions and 313 deletions

View File

@ -1,22 +1,40 @@
name: Gitea Actions Demo name: Gitea Android Builder
run-name: ${{ gitea.actor }} is testing out Gitea Actions run-name: ${{ gitea.actor }} is building an Android application
on: on:
push: push:
branches: branches:
- main - pipeline-test
jobs: jobs:
Explore-Gitea-Actions: Build:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event." - name: Checkout the repo
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner." - name: Set up JDK 23
- run: echo "The workflow is now ready to test your code on the runner." uses: actions/setup-java@v4
- name: List files in the repository with:
run: | java-version: '23'
ls ${{ gitea.workspace }} distribution: 'temurin'
- run: echo "This job's status is ${{ job.status }}." - name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: assembleDebug
- name: Upload .apk Artifact
uses: actions/upload-artifact@v3
with:
name: android-app-apk
path: app/build/outputs/apk/debug/*.apk
- name: Status
run: echo "This job's status is ${{ job.status }}."

View File

@ -0,0 +1,40 @@
name: Gitea Android Builder
run-name: ${{ gitea.actor }} is building an Android application
on:
push:
tags:
- "*"
jobs:
Build:
runs-on: ubuntu-24.04
steps:
- name: Checkout the repo
uses: actions/checkout@v4
- name: Set up JDK 23
uses: actions/setup-java@v4
with:
java-version: '23'
distribution: 'temurin'
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
- name: Build with Gradle
uses: gradle/gradle-build-action@v2
with:
arguments: assembleDebug
- name: Upload .apk Artifact
uses: actions/upload-artifact@v3
with:
name: android-app-apk
path: app/build/outputs/apk/debug/*.apk
- name: Status
run: echo "This job's status is ${{ job.status }}."

7
.gitignore vendored
View File

@ -1,12 +1,7 @@
*.iml *.iml
.gradle .gradle
/local.properties /local.properties
/.idea/caches /.idea
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store .DS_Store
/build /build
/captures /captures

3
.idea/.gitignore generated vendored
View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

1
.idea/.name generated
View File

@ -1 +0,0 @@
Bitcoin summarizer

View File

@ -1,123 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

6
.idea/compiler.xml generated
View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

19
.idea/gradle.xml generated
View File

@ -1,19 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

6
.idea/kotlinc.xml generated
View File

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

10
.idea/migrations.xml generated
View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>

10
.idea/misc.xml generated
View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

View File

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/vcs.xml generated
View File

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

View File

@ -1,3 +1,5 @@
# bitcoin-summarizer # bitcoin-summarizer
Open Source app that uses alternative.me API to check Bitcoin info Open Source app that uses alternative.me API to check Bitcoin info
:3

View File

@ -12,8 +12,8 @@ android {
applicationId = "ru.vendetti.bitcoin_summarizer" applicationId = "ru.vendetti.bitcoin_summarizer"
minSdk = 29 minSdk = 29
targetSdk = 35 targetSdk = 35
versionCode = 1 versionCode = 2
versionName = "1.0" versionName = "1.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@ -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)
@ -56,4 +57,12 @@ dependencies {
androidTestImplementation(libs.androidx.ui.test.junit4) androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest) debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.retrofit)
implementation(libs.converter.gson)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.vico.compose.m3)
implementation(libs.cicerone)
} }

View File

@ -1,15 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:name=".App"
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"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="false"
android:theme="@style/Theme.BitcoinSummarizer" android:theme="@style/Theme.BitcoinSummarizer"
tools:targetApi="31"> tools:targetApi="31">
<activity <activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

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,22 @@
package ru.vendetti.bitcoin_summarizer
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Query
// Retrofit-API-интерфейс
interface CryptoApiService {
// Получение индекса страха и жадности (по умолчанию за 30 дней)
@GET("fng/")
suspend fun getFearAndGreedIndex(
@Query("limit") limit: Int = 30
): Response<FearAndGreedResponse>
// Получение глобальной информации
@GET("v1/global/")
suspend fun getGlobalData(): Response<GlobalResponse>
// Получение данных Bitcoin
@GET("v1/ticker/bitcoin/")
suspend fun getBitcoinTicker(): Response<TickerResponse>
}

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,58 @@
package ru.vendetti.bitcoin_summarizer
//Репозиторий для обработки запросов
class CryptoRepository(private val apiService: CryptoApiService) {
/**
* Запрашивает данные для Bitcoin-тикера
*/
suspend fun fetchBitcoinTicker(): TickerResponse? {
return try {
val response = apiService.getBitcoinTicker()
if (response.isSuccessful) {
response.body()
} else {
println("Ошибка запроса Bitcoin Ticker: ${response.code()}")
null
}
} catch (e: Exception) {
println("Exception при получении Bitcoin Ticker: ${e.localizedMessage}")
null
}
}
/**
* Запрашивает глобальные данные крипторынка
*/
suspend fun fetchGlobalData(): GlobalResponse? {
return try {
val response = apiService.getGlobalData()
if (response.isSuccessful) {
response.body()
} else {
println("Ошибка запроса Global Data: ${response.code()}")
null
}
} catch (e: Exception) {
println("Exception при получении Global Data: ${e.localizedMessage}")
null
}
}
/**
* Запрашивает индекс страха и жадности за указанный период (по умолчанию 30 дней)
*/
suspend fun fetchFearAndGreedData(limit: Int = 30): FearAndGreedResponse? {
return try {
val response = apiService.getFearAndGreedIndex(limit)
if (response.isSuccessful) {
response.body()
} else {
println("Ошибка запроса Fear & Greed Index: ${response.code()}")
null
}
} catch (e: Exception) {
println("Exception при получении Fear & Greed Index: ${e.localizedMessage}")
null
}
}
}

View File

@ -0,0 +1,23 @@
package ru.vendetti.bitcoin_summarizer
import com.google.gson.annotations.SerializedName
// Обёртка для данных (тут вложенность есть и это API v2, а не v1, поэтому чуть сложнее структура, чем в GlobalResponse.kt)
data class FearAndGreedResponse(
@SerializedName("name")
val name: String,
@SerializedName("data")
val dataList: List<FearAndGreedData>
)
// Сами данные
data class FearAndGreedData(
@SerializedName("value")
val value: String,
@SerializedName("value_classification")
val valueClassification: String,
@SerializedName("timestamp")
val timestamp: String,
@SerializedName("time_until_update")
val timeUntilUpdate: String
)

View File

@ -0,0 +1,11 @@
package ru.vendetti.bitcoin_summarizer
import com.google.gson.annotations.SerializedName
// Данные
data class GlobalResponse(
@SerializedName("active_currencies") val activeCryptocurrencies: String = "",
@SerializedName("total_market_cap_usd") val totalMarketCapUsd: String = "",
@SerializedName("total_24h_volume_usd") val total24hVolumeUsd: String = "",
@SerializedName("bitcoin_percentage_of_market_cap") val bitcoinPercentageOfMarketCap: String = "0.0"
)

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

@ -1,47 +1,99 @@
package ru.vendetti.bitcoin_summarizer package ru.vendetti.bitcoin_summarizer
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.activity.enableEdgeToEdge import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize 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.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.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.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable 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.setValue
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.input.nestedscroll.nestedScroll
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.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
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.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 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.Green2
import java.text.DecimalFormat
import java.time.format.DateTimeFormatter
class MainActivity : FragmentActivity() {
private val navigator = AppNavigator(this, R.id.container)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() setContentView(R.layout.fragment_container_main)
setContent {
BitcoinSummarizerTheme { // start the root fragment
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> App.INSTANCE.router.newRootScreen(Screens.Crypto)
Greeting( }
name = "Android",
modifier = Modifier.padding(innerPadding) override fun onResumeFragments() {
) super.onResumeFragments()
} App.INSTANCE.navigatorHolder.setNavigator(navigator)
} }
}
} override fun onPause() {
} App.INSTANCE.navigatorHolder.removeNavigator()
super.onPause()
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello $name!",
modifier = modifier
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
BitcoinSummarizerTheme {
Greeting("Android")
} }
} }

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,16 @@
package ru.vendetti.bitcoin_summarizer
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitClient {
private const val BASE_URL = "https://api.alternative.me/"
val apiService: CryptoApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(CryptoApiService::class.java)
}
}

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

@ -0,0 +1,25 @@
package ru.vendetti.bitcoin_summarizer
import com.google.gson.annotations.SerializedName
// Обёртка для данных (тут json-массив с одним элементом, поэтому выглядит так)
typealias TickerResponse = List<TickerData>
// Сами данные
data class TickerData(
val id: String = "",
val name: String = "",
val symbol: String = "",
val rank: String = "",
@SerializedName("price_usd") val priceUsd: String = "0.0",
@SerializedName("price_btc") val priceBtc: String = "0.0",
@SerializedName("24h_volume_usd") val volume24hUsd: String = "0",
@SerializedName("market_cap_usd") val marketCapUsd: String = "0",
@SerializedName("available_supply") val availableSupply: String = "0",
@SerializedName("total_supply") val totalSupply: String = "0",
@SerializedName("max_supply") val maxSupply: String? = "0",
@SerializedName("percent_change_1h") val percentChange1h: String = "0",
@SerializedName("percent_change_24h") val percentChange24h: String = "0.0",
@SerializedName("percent_change_7d") val percentChange7d: String = "0.0",
@SerializedName("last_updated") val lastUpdated: String = ""
)

View File

@ -9,3 +9,11 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4) val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71) val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260) val Pink40 = Color(0xFF7D5260)
val EnglishViolet = Color(0xFF44355B)
val DarkPurple = Color(0xFF31263E)
val RaisinBlack = Color(0xFF221E22)
val HunyadiYellow = Color(0xFFECA72C)
val Flame = Color(0xFFEE5622)
val Green2 = Color(0xFFB1E434)

View File

@ -9,43 +9,45 @@ import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme( private val DarkColorScheme = darkColorScheme(
primary = Purple80, primary = RaisinBlack,
secondary = PurpleGrey80, secondary = DarkPurple,
tertiary = Pink80 tertiary = HunyadiYellow,
// Other default colors to override
background = DarkPurple,
surface = Color(0xFFFFFBFE),
onPrimary = HunyadiYellow,
onSecondary = HunyadiYellow,
onTertiary = DarkPurple,
onBackground = HunyadiYellow,
onSurface = Color(0xFF1C1B1F),
) )
private val LightColorScheme = lightColorScheme( private val LightColorScheme = lightColorScheme(
primary = Purple40, primary = HunyadiYellow,
secondary = PurpleGrey40, secondary = Flame,
tertiary = Pink40 tertiary = EnglishViolet,
/* Other default colors to override // Other default colors to override
background = Color(0xFFFFFBFE), background = Color.White,
surface = Color(0xFFFFFBFE), surface = Color(0xFFFFFBFE),
onPrimary = Color.White, onPrimary = RaisinBlack,
onSecondary = Color.White, onSecondary = Color.White,
onTertiary = Color.White, onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F), onBackground = RaisinBlack,
onSurface = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F),
*/
) )
@Composable @Composable
fun BitcoinSummarizerTheme( fun BitcoinSummarizerTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colorScheme = when { val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme darkTheme -> DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

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

View File

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

View File

@ -21,3 +21,5 @@ kotlin.code.style=official
# resources declared in the library itself and none from the library's dependencies, # resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
org.gradle.caching=true
org.gradle.parallel=true

View File

@ -1,16 +1,24 @@
[versions] [versions]
agp = "8.8.1" agp = "8.9.0"
converterGson = "2.9.0"
kotlin = "2.0.0" kotlin = "2.0.0"
coreKtx = "1.10.1" coreKtx = "1.15.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.2.1"
espressoCore = "3.5.1" espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.6.1" kotlinxCoroutinesAndroid = "1.10.1"
activityCompose = "1.8.0" kotlinxCoroutinesCore = "1.10.1"
composeBom = "2024.04.01" lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.1"
composeBom = "2025.02.00"
retrofit = "2.9.0"
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" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
@ -24,6 +32,12 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }

View File

@ -1,6 +1,6 @@
#Wed Feb 26 23:45:23 YEKT 2025 #Wed Feb 26 23:45:23 YEKT 2025
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists