diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..1f6132240 --- /dev/null +++ b/.gitignore @@ -0,0 +1,187 @@ +# Created by https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin +# Edit at https://www.toptal.com/developers/gitignore?templates=android,androidstudio,kotlin + +### Android ### +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof + +### Android Patch ### +gen-external-apklibs + +# Replacement of .externalNativeBuild directories introduced +# with Android Studio 3.5. + +### Kotlin ### +# Compiled class file +*.class + +# Log file + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# NDK +obj/ + +# IntelliJ IDEA +*.iws +/out/ + +# User-specific configurations +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml +.idea/navEditor.xml + +# Legacy Eclipse project files +.classpath +.project +.cproject +.settings/ + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Mongo Explorer plugin +.idea/mongoSettings.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### AndroidStudio Patch ### + +!/gradle/wrapper/gradle-wrapper.jar + +# End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,kotlin \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 000000000..f39e13760 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,176 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'dagger.hilt.android.plugin' + id 'org.jlleitschuh.gradle.ktlint' +} + +apply plugin: 'kotlin-kapt' + +android { + namespace 'com.javg.cryptocurrencies' + compileSdk 33 + + defaultConfig { + applicationId "com.javg.cryptocurrencies" + minSdk 23 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion '1.2.0' + } + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } + viewBinding { + enabled = true + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' + implementation 'androidx.activity:activity-compose:1.3.1' + implementation "androidx.compose.ui:ui:$compose_ui_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version" + implementation 'androidx.compose.material:material:1.2.0' + implementation 'androidx.databinding:databinding-runtime:7.1.2' + testImplementation 'junit:junit:4.12' + debugImplementation "androidx.compose.ui:ui-tooling:$compose_ui_version" + debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_ui_version" + + //testing + testImplementation 'junit:junit:4.13.2' + testImplementation 'io.mockk:mockk:1.11.0' + testImplementation 'androidx.test:core-ktx:1.4.0' + testImplementation 'androidx.test.ext:junit:1.1.3' + testImplementation 'com.google.truth:truth:1.1.3' + testImplementation 'androidx.test.espresso:espresso-core:3.5.1' + testImplementation 'org.robolectric:robolectric:4.8.1' + androidTestImplementation 'junit:junit:4.12' + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" + + //Retrofit + implementation "com.squareup.retrofit2:retrofit:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + implementation "com.squareup.okhttp3:logging-interceptor:$interceptor_version" + implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" + + //Dagger HILT + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-compiler:$hilt_version" + kapt "androidx.hilt:hilt-compiler:$hilt_compiler_version" + + // Lifecycle components + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + + //livedata jetpack compose + implementation "androidx.compose.runtime:runtime-livedata:$runtime_livedata_version" + + //constraintlayout + implementation "androidx.constraintlayout:constraintlayout:$constraintlayout_version" + implementation "androidx.constraintlayout:constraintlayout-compose:$constraintlayout_compose_version" + + //recycler view + implementation "androidx.recyclerview:recyclerview:$recyclerview_version" + implementation "androidx.recyclerview:recyclerview-selection:$recyclerview_selection_version" + + //activityviewModels + implementation "androidx.fragment:fragment-ktx:$fragment_ktx_version" + + // Kotlin + implementation "androidx.fragment:fragment:$fragment_version" + implementation "androidx.fragment:fragment-ktx:$fragment_version" + implementation "androidx.appcompat:appcompat:$appcompat_version" + implementation "androidx.legacy:legacy-support-v4:$legacy_support_version" + implementation "androidx.activity:activity-ktx:$activity_ktx_version" + + //card + implementation "androidx.cardview:cardview:$cardview_version" + + // + implementation "com.facebook.shimmer:shimmer:0.5.0" + + //glide + implementation "com.github.bumptech.glide:glide:$glide_version" + + //lottie + implementation "com.airbnb.android:lottie:$lottie_version" + + //room + implementation "androidx.room:room-runtime:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + + // To use Kotlin annotation processing tool (kapt) + kapt "androidx.room:room-compiler:$room_version" + // To use Kotlin Symbol Processing (KSP) + //ksp("androidx.room:room-compiler:$room_version") + + // optional - Kotlin Extensions and Coroutines support for Room + implementation "androidx.room:room-ktx:$room_version" + + // optional - RxJava2 support for Room + implementation "androidx.room:room-rxjava2:$room_version" + + // optional - RxJava3 support for Room + implementation "androidx.room:room-rxjava3:$room_version" + + // optional - Guava support for Room, including Optional and ListenableFuture + implementation "androidx.room:room-guava:$room_version" + + // optional - Test helpers + testImplementation "androidx.room:room-testing:$room_version" + + // optional - Paging 3 Integration + implementation "androidx.room:room-paging:$room_version" + + implementation "com.google.android.material:material:$googleMaterialVersion" + + //viewPager2 + implementation "androidx.viewpager2:viewpager2:$viewpager2_version" + + //navigation + implementation "androidx.navigation:navigation-fragment-ktx:$navigation_fragment_version" + implementation "androidx.navigation:navigation-ui-ktx:$navigation_fragment_version" + + //rxjava + implementation "io.reactivex.rxjava2:rxjava:$rxjava2_java_version" + implementation "io.reactivex.rxjava2:rxandroid:$rxjava2_android_version" + implementation "io.reactivex:rxandroid:$rx_android_version" + implementation "io.reactivex:rxjava:$rx_java_version" +} + +ktlint { + android = true + disabledRules = ["no-wildcard-imports"] +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/javg/cryptocurrencies/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/javg/cryptocurrencies/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..289404abd --- /dev/null +++ b/app/src/androidTest/java/com/javg/cryptocurrencies/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.javg.cryptocurrencies + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.javg.cryptocurrencies", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..05a599edd --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/empty_data.json b/app/src/main/assets/empty_data.json new file mode 100644 index 000000000..13a75943f --- /dev/null +++ b/app/src/main/assets/empty_data.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":60,"ip":0,"op":295,"w":426,"h":290,"nm":"search_empty","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"mouth","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":44,"s":[211,145,0],"to":[0,0.667,0],"ti":[0,-0.667,0]},{"t":69,"s":[211,149,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":44,"s":[100,100,100]},{"t":68,"s":[105,105,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.5,0.25],[-1.715,0.372]],"o":[[-13.025,-6.512],[7.5,-1.625]],"v":[[-10,12.25],[-31.75,12.625]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.454901960784,0.513725490196,0.580392156863,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"汗","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":51,"s":[0]},{"t":95,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":51,"s":[248,95.252,0],"to":[0,1.5,0],"ti":[0,-1.5,0]},{"t":94,"s":[248,104.252,0]}],"ix":2},"a":{"a":0,"k":[0,-19.333,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":51,"s":[40,40,100]},{"t":95,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-9.94,0],[0,7.27],[0,0],[0,-7.27]],"o":[[9.94,0],[0,-7.27],[0,0],[0,7.27]],"v":[[0,19],[18,5.84],[0,-19],[-18,5.84]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"gf","o":{"a":0,"k":100,"ix":10},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.69,0.976,1,0.5,0.714,0.954,0.997,1,0.738,0.932,0.994],"ix":9}},"s":{"a":0,"k":[0.019,-9.236],"ix":5},"e":{"a":0,"k":[10.208,19],"ix":6},"t":1,"nm":"Gradient Fill 1","mn":"ADBE Vector Graphic - G-Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"汗","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":569,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"eye","parent":1,"sr":1,"ks":{"o":{"a":0,"k":92,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-7.064,-11.347,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":3,"s":[100,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":9,"s":[100,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":12,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":50,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":53,"s":[100,20,100]},{"t":56,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[8.466,16.135],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"eye","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":569,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"eye","parent":1,"sr":1,"ks":{"o":{"a":0,"k":92,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-33.767,-11.347,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":3,"s":[100,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":6,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":9,"s":[100,20,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":12,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":50,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":53,"s":[100,20,100]},{"t":56,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[8.466,16.135],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"eye","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":569,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"magnifier","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[213,145,0],"ix":2},"a":{"a":0,"k":[213,145,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":426,"h":290,"ip":0,"op":569,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[213,145,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.435294117647,0.498039215686,0.564705882353,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"橢圓形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-5,"ix":10},"p":{"a":0,"k":[190.106,142.222,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[99.994,100.091],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-40,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"橢圓形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"橢圓形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-5,"ix":10},"p":{"a":0,"k":[192.013,142.223,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-26.81,22.62],[22.49,26.96],[26.8,-22.62],[-22.5,-26.95]],"o":[[26.81,-22.62],[-22.49,-26.96],[-26.81,22.62],[22.49,26.96]],"v":[[40.727,48.812],[48.537,-40.958],[-40.723,-48.808],[-48.533,40.952]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.6392156862745098,0.7490196078431373,0.7764705882352941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"橢圓形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"矩形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-5,"ix":10},"p":{"a":0,"k":[268.024,226.941,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[42.094,74.507],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":10,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.6392156862745098,0.7490196078431373,0.7764705882352941,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-40,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"矩形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"矩形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-5,"ix":10},"p":{"a":0,"k":[238.933,197.25,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[24.446,44.702],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.8745098039215686,0.8980392156862745,0.9333333333333333,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":-40,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"矩形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"合併形狀","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":60,"s":[0]},{"t":265,"s":[360]}],"ix":10},"p":{"a":0,"k":[208,27,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[2,18],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 2","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"mm","mm":2,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.792156862745098,0.8313725490196079,0.8392156862745098,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"合併形狀","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"合併形狀","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":358,"s":[360]}],"ix":10},"p":{"a":0,"k":[418,107,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[2,18],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 2","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"mm","mm":2,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.796078026295,0.835294008255,0.843137025833,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"合併形狀","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"合併形狀","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":5,"s":[0]},{"t":281,"s":[360]}],"ix":10},"p":{"a":0,"k":[31,196,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[16,2],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"rc","d":1,"s":{"a":0,"k":[2,18],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 2","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"mm","mm":2,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.796078026295,0.835294008255,0.843137025833,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"合併形狀","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"橢圓形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":281,"s":[360]}],"ix":10},"p":{"a":0,"k":[70,33,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.701960980892,0.756862998009,0.776471018791,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"橢圓形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}]},{"id":"comp_3","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"橢圓形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[306,9,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.701960980892,0.756862998009,0.776471018791,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"橢圓形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"橢圓形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[376,179,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.701960980892,0.768626987934,0.776471018791,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"橢圓形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"橢圓形","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[8,115,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[16,16],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.701960980892,0.768626987934,0.776471018791,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"橢圓形","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"shadow","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[191,145,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[70,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":30,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":60,"s":[70,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":90,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":120,"s":[70,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":150,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":180,"s":[70,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":210,"s":[50,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":240,"s":[70,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":270,"s":[50,100,100]},{"t":295,"s":[70,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[56.697,6.631],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0.2901960784313726,0.5098039215686274,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-9.651,124.815],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"main","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[213,145,0],"to":[0,0.833,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[213,150,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":60,"s":[213,145,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":90,"s":[213,150,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":120,"s":[213,145,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":150,"s":[213,150,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":180,"s":[213,145,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":210,"s":[213,150,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":240,"s":[213,145,0],"to":[0,0,0],"ti":[0,-0.833,0]},{"t":270,"s":[213,150,0]}],"ix":2},"a":{"a":0,"k":[213,145,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"w":426,"h":290,"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"decoration1","refId":"comp_2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.981],"y":[1.128]},"o":{"x":[0.39],"y":[0.019]},"t":8,"s":[0]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":40,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":72,"s":[80]},{"t":73,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[213,289,0],"ix":2},"a":{"a":0,"k":[213,289,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.667],"y":[0.866,0.866,1]},"o":{"x":[0.37,0.37,0.333],"y":[-0.029,-0.029,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":40,"s":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":72,"s":[80,80,100]},{"t":133,"s":[90,90,100]}],"ix":6}},"ao":0,"w":426,"h":290,"ip":0,"op":569,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"decoratio2","refId":"comp_3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":41,"s":[100]},{"t":72,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[211,291,0],"ix":2},"a":{"a":0,"k":[211,291,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[0,0,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":43,"s":[99.442,99.442,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":72,"s":[84.442,84.442,100]},{"t":133,"s":[90,90,100]}],"ix":6}},"ao":0,"w":426,"h":290,"ip":0,"op":569,"st":0,"bm":0}],"markers":[{"tm":180,"cm":"1","dr":0},{"tm":3600,"cm":"2","dr":0}]} \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 000000000..f2e45dcaf Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/javg/cryptocurrencies/CRYSplashActivity.kt b/app/src/main/java/com/javg/cryptocurrencies/CRYSplashActivity.kt new file mode 100644 index 000000000..ce1f912ec --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/CRYSplashActivity.kt @@ -0,0 +1,16 @@ +package com.javg.cryptocurrencies + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +@SuppressLint("CustomSplashScreen") +class CRYSplashActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + startActivity(Intent(this, MainActivity::class.java)) + finish() + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/MainActivity.kt b/app/src/main/java/com/javg/cryptocurrencies/MainActivity.kt new file mode 100644 index 000000000..1b5cb0fb9 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/MainActivity.kt @@ -0,0 +1,39 @@ +package com.javg.cryptocurrencies + +import android.os.Bundle +import androidx.navigation.NavController +import androidx.navigation.NavGraph +import androidx.navigation.fragment.NavHostFragment +import com.javg.cryptocurrencies.view.base.CRYBaseActivity +import dagger.hilt.android.AndroidEntryPoint + +/** + * @author Juan Antonio Vera + * Date modified 22/02/2023 + * + * Contains the functionality and configuration to start + * the application and its navigation + * + * @since 2.0 + */ +@AndroidEntryPoint +class MainActivity : CRYBaseActivity() { + + private lateinit var navController: NavController + private lateinit var navHostFragment: NavHostFragment + private lateinit var navGraph: NavGraph + + override fun onCreate(savedInstanceState: Bundle?) { + setTheme(R.style.Theme_Cryptocurrencies) + super.onCreate(savedInstanceState) + setContentView(R.layout.cry_nav_host_main) + setUpNavHost() + } + + private fun setUpNavHost() { + navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_main) as NavHostFragment + navGraph = navHostFragment.navController.navInflater.inflate(R.navigation.cry_nav_graph_main) + navController = navHostFragment.navController + navController.graph = navGraph + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/config/CRYApplication.kt b/app/src/main/java/com/javg/cryptocurrencies/config/CRYApplication.kt new file mode 100644 index 000000000..7b38c9112 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/config/CRYApplication.kt @@ -0,0 +1,7 @@ +package com.javg.cryptocurrencies.config + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class CRYApplication : Application() diff --git a/app/src/main/java/com/javg/cryptocurrencies/config/CRYDispatcherModule.kt b/app/src/main/java/com/javg/cryptocurrencies/config/CRYDispatcherModule.kt new file mode 100644 index 000000000..535bcfcd2 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/config/CRYDispatcherModule.kt @@ -0,0 +1,20 @@ +package com.javg.cryptocurrencies.config + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.Dispatchers +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CRYDispatcherModule { + + /** + * Provides the trigger where the task will be executed + */ + @Provides + @Singleton + fun providerDispatcher() = Dispatchers.IO +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/config/CRYRetrofitHelper.kt b/app/src/main/java/com/javg/cryptocurrencies/config/CRYRetrofitHelper.kt new file mode 100644 index 000000000..4aff5af00 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/config/CRYRetrofitHelper.kt @@ -0,0 +1,70 @@ +package com.javg.cryptocurrencies.config + +import com.javg.cryptocurrencies.data.network.CRYApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CRYRetrofitHelper { + /** + * Takes care of providing an instance of the api interface using a retrofit construct + */ + @Provides + @Singleton + fun providerApi(builder: Retrofit.Builder): CRYApi { + return builder.build().create(CRYApi::class.java) + } + + /** + * Provide a retrofit instance already with your build + * + * @param httpClient is a builder type model + */ + @Provides + @Singleton + fun providerRetrofit(httpClient: OkHttpClient.Builder): Retrofit.Builder { + return Retrofit.Builder() + .baseUrl(CRYApi.BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .client(httpClient.build()) + } + + /** + * It is responsible for providing an instance of an httpClient with + * its configuration to add a header and enable the body of the requests + * + * @param logging It is a model that contains the configuration to see + * the body of the requests + */ + @Provides + @Singleton + fun providerHttpClient(logging: HttpLoggingInterceptor): OkHttpClient.Builder { + return OkHttpClient.Builder() + .addInterceptor { chain -> + val request = chain.request() + .newBuilder() + .addHeader("User-Agent", "CryptocurrenciesApp") + .build() + chain.proceed(request) + }.addInterceptor(logging) + } + + /** + * Provides the instance of an interceptor + */ + @Provides + @Singleton + fun providerHttpLoggingInterceptor(): HttpLoggingInterceptor = HttpLoggingInterceptor().apply { + setLevel(HttpLoggingInterceptor.Level.BODY) + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/config/CRYRoomHelper.kt b/app/src/main/java/com/javg/cryptocurrencies/config/CRYRoomHelper.kt new file mode 100644 index 000000000..8dce16ac7 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/config/CRYRoomHelper.kt @@ -0,0 +1,50 @@ +package com.javg.cryptocurrencies.config + +import android.content.Context +import androidx.room.Room +import com.javg.cryptocurrencies.data.db.remote.CRYAppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object CRYRoomHelper { + /** + * It is responsible for providing an instance of the room + * database with the necessary configuration + * + * @param appContext is the application context + */ + @Singleton + @Provides + fun provideAppDatabase(@ApplicationContext appContext: Context): CRYAppDatabase { + return Room + .databaseBuilder( + appContext, + CRYAppDatabase::class.java, + CRYAppDatabase.DB_NAME, + ) + .fallbackToDestructiveMigration() + .build() + } + + /** + * Provides an instance of the interface that controls the books + * table with their respective queries + */ + @Singleton + @Provides + fun provideBookDao(db: CRYAppDatabase) = db.bookDao() + + /** + * Provides an instance of the interface that controls the detail + * table of a specific book with its respective queries. + */ + @Singleton + @Provides + fun provideTickerDao(db: CRYAppDatabase) = db.tickerDao() +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/db/dao/CRYBookDao.kt b/app/src/main/java/com/javg/cryptocurrencies/data/db/dao/CRYBookDao.kt new file mode 100644 index 000000000..0627fd7b7 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/db/dao/CRYBookDao.kt @@ -0,0 +1,37 @@ +package com.javg.cryptocurrencies.data.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.javg.cryptocurrencies.data.db.entity.CRYBookEntity +import com.javg.cryptocurrencies.data.db.remote.CRYAppDatabase +import kotlinx.coroutines.flow.Flow + +/** + * @author Juan Vera Gomez + * Date modified 13/02/2023 + * + * Contains the functions for the crud database of books + * + * @since 2.0 + */ +@Dao +interface CRYBookDao { + + /** + * Returns a list of books from the books table saved in the database + */ + @Query("SELECT * FROM ${CRYAppDatabase.BOOK_TABLE}") + fun getAllBook(): List + + @Query("SELECT * FROM ${CRYAppDatabase.BOOK_TABLE}") + fun getAllBookV2(): Flow> + + /** + * It is in charge of saving the entire list of books obtained remotely from the api + */ + @Insert(onConflict = OnConflictStrategy.REPLACE) + @JvmSuppressWildcards + fun insertAll(books: List) +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/db/dao/CRYTickerDao.kt b/app/src/main/java/com/javg/cryptocurrencies/data/db/dao/CRYTickerDao.kt new file mode 100644 index 000000000..51216b9a0 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/db/dao/CRYTickerDao.kt @@ -0,0 +1,20 @@ +package com.javg.cryptocurrencies.data.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.javg.cryptocurrencies.data.db.entity.CRYDetailBookEntity +import com.javg.cryptocurrencies.data.db.remote.CRYAppDatabase + +@Dao +interface CRYTickerDao { + @Query("SELECT * FROM ${CRYAppDatabase.DETAIL_BOOK_TABLE} WHERE book = :book") + fun findById(book: String): CRYDetailBookEntity? + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insert(ticker: CRYDetailBookEntity) + + @Query("UPDATE ${CRYAppDatabase.DETAIL_BOOK_TABLE} SET askList = :ask, bidsList = :bids WHERE book = :book") + fun update(ask: String, bids: String, book: String) +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/db/entity/CRYEntity.kt b/app/src/main/java/com/javg/cryptocurrencies/data/db/entity/CRYEntity.kt new file mode 100644 index 000000000..1ab4c30ae --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/db/entity/CRYEntity.kt @@ -0,0 +1,27 @@ +package com.javg.cryptocurrencies.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.javg.cryptocurrencies.data.db.remote.CRYAppDatabase + +@Entity(tableName = CRYAppDatabase.BOOK_TABLE) +data class CRYBookEntity( + @PrimaryKey + @ColumnInfo(name = "book") + val book: String, + @ColumnInfo(name = "minimumPrice") val minimumPrice: String, + @ColumnInfo(name = "maximumPrice") val maximumPrice: String, +) + +@Entity(tableName = CRYAppDatabase.DETAIL_BOOK_TABLE) +data class CRYDetailBookEntity( + @PrimaryKey + @ColumnInfo("book") + val book: String, + @ColumnInfo("high") val high: String, + @ColumnInfo("last") val last: String, + @ColumnInfo("low") val low: String, + @ColumnInfo("askList") val askList: String, + @ColumnInfo("bidsList") val bidsList: String, +) diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/db/remote/CRYAppDatabase.kt b/app/src/main/java/com/javg/cryptocurrencies/data/db/remote/CRYAppDatabase.kt new file mode 100644 index 000000000..0e41dc12a --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/db/remote/CRYAppDatabase.kt @@ -0,0 +1,20 @@ +package com.javg.cryptocurrencies.data.db.remote + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.javg.cryptocurrencies.data.db.dao.CRYBookDao +import com.javg.cryptocurrencies.data.db.dao.CRYTickerDao +import com.javg.cryptocurrencies.data.db.entity.CRYBookEntity +import com.javg.cryptocurrencies.data.db.entity.CRYDetailBookEntity + +@Database(entities = [CRYBookEntity::class, CRYDetailBookEntity::class], version = 4) +abstract class CRYAppDatabase : RoomDatabase() { + companion object { + const val DB_NAME = "database_crypto_book" + const val BOOK_TABLE = "book_table" + const val DETAIL_BOOK_TABLE = "detail_book_table" + } + abstract fun bookDao(): CRYBookDao + + abstract fun tickerDao(): CRYTickerDao +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/domain/CRYBookUseCase.kt b/app/src/main/java/com/javg/cryptocurrencies/data/domain/CRYBookUseCase.kt new file mode 100644 index 000000000..621c0b76d --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/domain/CRYBookUseCase.kt @@ -0,0 +1,85 @@ +package com.javg.cryptocurrencies.data.domain + +import com.javg.cryptocurrencies.data.model.CRYBook +import com.javg.cryptocurrencies.data.repository.CRYBookRepository +import com.javg.cryptocurrencies.utils.getSecondCoinsText +import com.javg.cryptocurrencies.utils.separateStringCoins +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +/** + * @author Juan Vera Gomez + * Date modified 22/02/2023 + * + * Contains the necessary function to get the information + * from books to the repository layer + * + * @param repository It is the repository that is responsible for obtaining the books + * + * @since 2.1 + */ +class CRYBookUseCase @Inject constructor( + private val repository: CRYBookRepository, +) { + /** + * Transform the list to assign the name of the simple book and add + * the corresponding images to the main moment and the one to be converted + * + * @param books is the list a list of books + * @return a transformed list + */ + fun transformBooks(books: List): List { + books.map { + it.singleBook = it.book.separateStringCoins() + it.imageUrl = "https://cryptoicons.org/api/icon/${it.book.separateStringCoins()}/200" + it.bookDestination = "https://cryptoicons.org/api/icon/${it.book.getSecondCoinsText()}/200" + } + return books + } + + /** + * You are going to create a hashmap taking a list as a reference, + * which will have the name of the coin as a key and a list + * of all the coins that the main list has as a value to classify it. + * + * @param books is the list a list of books + * @return a hashmap with the name of the coin as the key and the + * list of all matches as the value + */ + fun createUniqueMap(books: List): HashMap> { + val mapBooks = HashMap>() + val uniqueBooks: List = books + .distinctBy { it.singleBook } + .sortedBy { it.singleBook } + + uniqueBooks.forEach { uniqueBook -> + val booksGroup = books.filter { it.singleBook == uniqueBook.singleBook } + mapBooks[uniqueBook.singleBook] = booksGroup + } + + return mapBooks + } + + /** + * Create a list distinguishing the first name of the book + * and ordering it alphabetically + * + * @param books is the list a list of books + * @return a list without repeated and ordered books + */ + fun createListBookTitles(books: List): List { + return books + .distinctBy { it.singleBook } + .sortedBy { it.singleBook } + } + + /** + * Gets the list of books that are stored in the database + */ + fun queryBooks(): Flow> = repository.queryBooks() + + /** + * Query the api to get the books remotely + */ + fun getAvailableBooksRx(): Boolean = repository.getAvailableBooksRx() +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/domain/CRYGetListBookWithTickerUseCase.kt b/app/src/main/java/com/javg/cryptocurrencies/data/domain/CRYGetListBookWithTickerUseCase.kt new file mode 100644 index 000000000..3c433dea1 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/domain/CRYGetListBookWithTickerUseCase.kt @@ -0,0 +1,46 @@ +package com.javg.cryptocurrencies.data.domain + +import com.javg.cryptocurrencies.data.model.CRYDetailBook +import com.javg.cryptocurrencies.data.repository.CRYOrderBookRepository +import com.javg.cryptocurrencies.data.repository.CRYTickerRepository +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/** + * @author Juan Vera Gomez + * Date modified 13/02/2023 + * + * Contains the functionality to query the information and make + * a deal before returning the information to the view model + * + * @param tickerRepository is the repository that returns only ticker information + * @param orderBookRepository is the repository that returns only order book information + * + * @since 2.0 + */ +class CRYGetListBookWithTickerUseCase @Inject constructor( + private val tickerRepository: CRYTickerRepository, + private val orderBookRepository: CRYOrderBookRepository, + private val dispatcher: CoroutineDispatcher, +) { + + /** + * Returns a detail type model of a specific book, + * obtaining the information from two different repositories + * + * @param book is the name of the book to consult its information + * + * @return CRYDetailBook is the detail model of the consulted book + */ + suspend operator fun invoke(book: String): CRYDetailBook? = withContext(dispatcher) { + val ticker = tickerRepository.getTicker(book) + var detailBook: CRYDetailBook? = null + + if (ticker != null) { + detailBook = orderBookRepository.getOrderBook(book) + } + + detailBook + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/mapper/CRYMapper.kt b/app/src/main/java/com/javg/cryptocurrencies/data/mapper/CRYMapper.kt new file mode 100644 index 000000000..71be03cbb --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/mapper/CRYMapper.kt @@ -0,0 +1,74 @@ +package com.javg.cryptocurrencies.data.mapper + +import com.javg.cryptocurrencies.data.db.entity.CRYBookEntity +import com.javg.cryptocurrencies.data.db.entity.CRYDetailBookEntity +import com.javg.cryptocurrencies.data.model.CRYBook +import com.javg.cryptocurrencies.data.model.CRYBookResponse +import com.javg.cryptocurrencies.data.model.CRYDetailBook +import com.javg.cryptocurrencies.data.model.CRYTicker +import com.javg.cryptocurrencies.utils.CRYUtils + +/** + * A function is extended to be able to treat the model + * of the response to an entity model + */ +fun CRYBookResponse.toEntity(): CRYBookEntity { + return CRYBookEntity( + book = this.book, + minimumPrice = this.minimumPrice, + maximumPrice = this.maximumPrice, + ) +} + +/** + * A function is extended from the entity model to + * handle it and pass it to a general model + */ +fun CRYBookEntity.toDomain(): CRYBook { + return CRYBook( + book = this.book, + imageUrl = "", + bookDestination = "", + ) +} + +/** + * A function is extended to be able to treat the model + * of the response to an entity model + */ +fun CRYTicker.toEntity(): CRYDetailBookEntity { + return CRYDetailBookEntity( + book = this.book, + high = this.high, + last = this.last, + low = this.low, + askList = "", + bidsList = "", + ) +} + +/** + * A function is extended from the entity model to + * handle it and pass it to a general model + */ +fun CRYDetailBookEntity.toDomain(): CRYDetailBook { + return CRYDetailBook( + high = this.high, + last = this.last, + low = this.low, + ) +} + +/** + * An entity model function is extended to handle it and pass + * it to a general model by updating missing data + */ +fun CRYDetailBookEntity.toDomainAll(): CRYDetailBook { + return CRYDetailBook( + high = this.high, + last = this.last, + low = this.low, + askList = CRYUtils.convertersJsonToList(this.askList), + bidsList = CRYUtils.convertersJsonToList(this.bidsList), + ) +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/model/CRYModel.kt b/app/src/main/java/com/javg/cryptocurrencies/data/model/CRYModel.kt new file mode 100644 index 000000000..abac831b2 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/model/CRYModel.kt @@ -0,0 +1,16 @@ +package com.javg.cryptocurrencies.data.model + +data class CRYBook( + var book: String, + var imageUrl: String, + var bookDestination: String, + var singleBook: String = "", +) + +data class CRYDetailBook( + var high: String = "", + var last: String = "", + var low: String = "", + var askList: List? = null, + var bidsList: List? = null, +) diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/model/CRYResponseModel.kt b/app/src/main/java/com/javg/cryptocurrencies/data/model/CRYResponseModel.kt new file mode 100644 index 000000000..934c13270 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/model/CRYResponseModel.kt @@ -0,0 +1,70 @@ +package com.javg.cryptocurrencies.data.model + +import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName + +open class CRYBaseResponse { + @SerializedName("success") + @Expose + var success: Boolean = false + + @SerializedName("payload") + @Expose + var payload: T? = null +} + +data class CRYBookResponse( + @SerializedName("book") + var book: String, + @SerializedName("minimum_price") + var minimumPrice: String, + @SerializedName("maximum_price") + var maximumPrice: String, + @SerializedName("minimum_amount") + var minimumAmount: String, + @SerializedName("maximum_amount") + var maximumAmount: String, + @SerializedName("minimum_value") + var minimumValue: String, + @SerializedName("maximum_value") + var maximumValue: String, + @SerializedName("tick_size") + var tickSize: String, + @SerializedName("default_chart") + var defaultChart: String, +) + +data class CRYTicker( + @SerializedName("book") + var book: String, + @SerializedName("volume") + var volume: String, + @SerializedName("high") + var high: String, + @SerializedName("last") + var last: String, + @SerializedName("low") + var low: String, + @SerializedName("vwap") + var vwap: String, + @SerializedName("ask") + var ask: String, + @SerializedName("bid") + var bid: String, +) + +data class CRYOrderBook( + @SerializedName("bids") + var bidsList: List, + @SerializedName("asks") + var asksList: List, +) + +data class CRYAskOrBids( + @SerializedName("book") + var book: String, + @SerializedName("price") + var price: String, + @SerializedName("amount") + var amount: String, +) diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/network/CRYApi.kt b/app/src/main/java/com/javg/cryptocurrencies/data/network/CRYApi.kt new file mode 100644 index 000000000..22f0cf599 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/network/CRYApi.kt @@ -0,0 +1,31 @@ +package com.javg.cryptocurrencies.data.network + +import com.javg.cryptocurrencies.data.model.CRYBaseResponse +import com.javg.cryptocurrencies.data.model.CRYBookResponse +import com.javg.cryptocurrencies.data.model.CRYOrderBook +import com.javg.cryptocurrencies.data.model.CRYTicker +import retrofit2.http.GET +import retrofit2.http.Query +import rx.Observable + +interface CRYApi { + + companion object { + const val BASE_URL = "https://api.bitso.com/" + const val END_POINT_AVAILABLE_BOOKS = "v3/available_books" + const val END_POINT_ORDER_BOOK = "v3/order_book" + const val END_POINT_TICKER = "v3/ticker" + } + + @GET(END_POINT_AVAILABLE_BOOKS) + suspend fun getListAvailableBooks(): CRYBaseResponse> + + @GET(END_POINT_AVAILABLE_BOOKS) + fun getListAvailableBooksRX(): Observable>> + + @GET(END_POINT_TICKER) + suspend fun getTicker(@Query("book") book: String): CRYBaseResponse + + @GET(END_POINT_ORDER_BOOK) + suspend fun getOrderBook(@Query("book") book: String): CRYBaseResponse +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYBookRepository.kt b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYBookRepository.kt new file mode 100644 index 000000000..f35fadd10 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYBookRepository.kt @@ -0,0 +1,75 @@ +package com.javg.cryptocurrencies.data.repository + +import android.content.Context +import android.util.Log +import com.javg.cryptocurrencies.data.db.dao.CRYBookDao +import com.javg.cryptocurrencies.data.mapper.toDomain +import com.javg.cryptocurrencies.data.mapper.toEntity +import com.javg.cryptocurrencies.data.model.CRYBook +import com.javg.cryptocurrencies.data.network.CRYApi +import com.javg.cryptocurrencies.utils.CRYUtils +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import javax.inject.Inject + +/** + * @author Juan Vera Gomez + * Date modified 10/02/2023 + * + * It is in charge of the functionality to be able to obtain the information + * from the database and in case of not having information, + * obtain it remotely through a service + * + * @since 2.0 + */ +class CRYBookRepository @Inject constructor( + @ApplicationContext val context: Context, + private val cryApi: CRYApi, + private val bookDao: CRYBookDao, +) : CRYGenericRepository() { + + /** + * Observe through a flow the list of books stored in the database every time it changes + * + * @return a book list type flow + */ + fun queryBooks(): Flow> = bookDao.getAllBookV2().map { books -> + books.map { it.toDomain() } + } + + /** + * It is in charge of consuming the remote api to be able + * to obtain the list of books and later store them in the database + */ + fun getAvailableBooksRx(): Boolean { + var result = false + cryApi.getListAvailableBooksRX() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + result = if (response.payload.isNullOrEmpty()) { + false + } else { + CRYUtils.saveTime(context) + response?.payload?.let { payload -> + val booksEntity = payload.map { it.toEntity() } + + Observable.just(bookDao) + .subscribeOn(Schedulers.io()) + .subscribe { + it.insertAll(booksEntity) + } + } + true + } + }) { throwable -> + Log.e("CRYBookRepository", "Error $throwable") + result = false + } + return result + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYGenericRepository.kt b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYGenericRepository.kt new file mode 100644 index 000000000..f96812cd4 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYGenericRepository.kt @@ -0,0 +1,13 @@ +package com.javg.cryptocurrencies.data.repository + +open class CRYGenericRepository { + suspend fun getResponse( + callFunction: suspend () -> T, + ) { + try { + callFunction.invoke() + } catch (e: Exception) { + println("exception getResponse -> ${e.message}") + } + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYOrderBookRepository.kt b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYOrderBookRepository.kt new file mode 100644 index 000000000..1d984a341 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYOrderBookRepository.kt @@ -0,0 +1,58 @@ +package com.javg.cryptocurrencies.data.repository + +import com.javg.cryptocurrencies.data.db.dao.CRYTickerDao +import com.javg.cryptocurrencies.data.mapper.toDomainAll +import com.javg.cryptocurrencies.data.model.CRYDetailBook +import com.javg.cryptocurrencies.data.network.CRYApi +import com.javg.cryptocurrencies.utils.CRYUtils +import javax.inject.Inject + +/** + * @author Juan Vera Gomez + * Date modified 13/02/2023 + * + * allows you to retrieve a list of all open orders in + * the specified book. + * + * @param cryApi is an interface that contains the remote query endpoints + * + * @since 2.0 + */ +class CRYOrderBookRepository @Inject constructor( + private val cryApi: CRYApi, + private val tickerDao: CRYTickerDao, +) { + + /** + * Returns a book detail model obtaining the information from the database or, + * if it is empty, consult it with the remote api + * + * @param book is the name of the book to consult its specific information + */ + suspend fun getOrderBook(book: String): CRYDetailBook { + var ticker = tickerDao.findById(book) + + ticker?.let { detailBookEntity -> + if (detailBookEntity.askList.isEmpty() || detailBookEntity.bidsList.isEmpty()) { + val response = getOrderBookFromApi(book) + response.payload?.let { + tickerDao.update( + ask = CRYUtils.convertersListToJson(it.asksList), + bids = CRYUtils.convertersListToJson(it.bidsList), + book = book, + ) + } + ticker = tickerDao.findById(book) + } + } + + return ticker?.toDomainAll() ?: CRYDetailBook() + } + + /** + * Returns a book order type model consulting the information remotely to an api + * + * @param book is the name of the book to consult its specific information + */ + private suspend fun getOrderBookFromApi(book: String) = cryApi.getOrderBook(book) +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYTickerRepository.kt b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYTickerRepository.kt new file mode 100644 index 000000000..a0caf8f7e --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/data/repository/CRYTickerRepository.kt @@ -0,0 +1,54 @@ +package com.javg.cryptocurrencies.data.repository + +import android.content.Context +import com.javg.cryptocurrencies.data.db.dao.CRYTickerDao +import com.javg.cryptocurrencies.data.db.entity.CRYDetailBookEntity +import com.javg.cryptocurrencies.data.mapper.toDomain +import com.javg.cryptocurrencies.data.mapper.toEntity +import com.javg.cryptocurrencies.data.model.CRYBaseResponse +import com.javg.cryptocurrencies.data.model.CRYDetailBook +import com.javg.cryptocurrencies.data.model.CRYTicker +import com.javg.cryptocurrencies.data.network.CRYApi +import com.javg.cryptocurrencies.utils.CRYUtils +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +/** + * @author Juan Vera Gomez + * Date modified 16/02/2023 + * + * Allows you to retrieve business information from the specified book. + * + * @param cryApi is an interface that contains the remote query endpoints + * + * @since 2.0 + */ +class CRYTickerRepository @Inject constructor( + @ApplicationContext val context: Context, + private val cryApi: CRYApi, + private val tickerDao: CRYTickerDao, +) : CRYGenericRepository() { + + /** + * + */ + suspend fun getTicker(book: String): CRYDetailBook? { + var ticker = tickerDao.findById(book) + + if (ticker == null) { + val tickerAux = getTickerFromApi(book) + tickerAux?.let { + CRYUtils.saveTime(context) + tickerDao.insert(it) + } + ticker = tickerDao.findById(book) + } + return ticker?.toDomain() + } + + private suspend fun getTickerFromApi(book: String): CRYDetailBookEntity? { + var response = CRYBaseResponse() + getResponse { response = cryApi.getTicker(book) } + return response.payload?.toEntity() + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/utils/CRYUtils.kt b/app/src/main/java/com/javg/cryptocurrencies/utils/CRYUtils.kt new file mode 100644 index 000000000..e12012c04 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/utils/CRYUtils.kt @@ -0,0 +1,117 @@ +package com.javg.cryptocurrencies.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities +import android.os.Build +import android.util.Log +import com.google.gson.Gson +import com.javg.cryptocurrencies.data.model.CRYAskOrBids +import java.text.SimpleDateFormat +import java.util.* + +object CRYUtils { + + /** + * Checks if the device is connected to a network source, + * be it mobile or wifi and returns a true or false + * depending on whether or not it has a connection + */ + fun isInternetAvailable(context: Context, onChangeState: (isConnected: Boolean) -> Unit): Boolean { + val TAG = "isInternetAvailable" + var result = false + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val networkCapabilities = connectivityManager.activeNetwork ?: return false + val actNw = + connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false + result = when { + actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true + actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true + actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true + else -> false + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + connectivityManager.registerDefaultNetworkCallback(object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + onChangeState.invoke(true) + Log.e(TAG, "====================== The default network is now: " + network) + } + + override fun onLost(network: Network) { + onChangeState.invoke(false) + Log.e(TAG, "====================== The application no longer has a default network. The last default network was " + network) + } + + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + Log.e(TAG, "====================== The default network changed capabilities: " + networkCapabilities) + } + + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + Log.e(TAG, "====================== The default network changed link properties: " + linkProperties) + } + }) + } + } else { + connectivityManager.run { + connectivityManager.activeNetworkInfo?.run { + result = when (type) { + ConnectivityManager.TYPE_WIFI -> true + ConnectivityManager.TYPE_MOBILE -> true + ConnectivityManager.TYPE_ETHERNET -> true + else -> false + } + } + } + } + + return result + } + + /** + * Gets device time with mexico time zone setting and returns it + */ + @SuppressLint("SimpleDateFormat") + fun getCurrentDay(): String { + val myTimeZone = TimeZone.getTimeZone("America/Mexico_City") + val simpleDateFormat = SimpleDateFormat("HH:mm:ss") + simpleDateFormat.timeZone = myTimeZone + return simpleDateFormat.format(Date()) + } + + /** + * Save a time in device preferences + * + * @param context is the context of the activity + */ + fun saveTime(context: Context) { + val sharedPreference = context.getSharedPreferences("PREFERENCE_NAME", Context.MODE_PRIVATE) + val editor = sharedPreference.edit() + editor.putString("time", getCurrentDay()) + editor.apply() + } + + /** + * Returns the time saved in device preferences + * + * @param context is the context of the activity + */ + fun getSaveTime(context: Context): String { + val sharedPreference = context.getSharedPreferences("PREFERENCE_NAME", Context.MODE_PRIVATE) + return sharedPreference.getString("time", "") ?: "" + } + + /** + * Convert a list of type CRYAskOrBids to a string + */ + fun convertersListToJson(askOrBids: List): String = Gson().toJson(askOrBids) + + /** + * Converts a string to a list of type CRYAskOrBids + */ + fun convertersJsonToList(askOrBids: String) = Gson().fromJson(askOrBids, Array::class.java).toList() +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/utils/CryStringExt.kt b/app/src/main/java/com/javg/cryptocurrencies/utils/CryStringExt.kt new file mode 100644 index 000000000..404a4cd7d --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/utils/CryStringExt.kt @@ -0,0 +1,16 @@ +package com.javg.cryptocurrencies.utils + +/** + * Function that extends a string to be able to separate the text by a '_' + * symbol and return the first element separated in it + */ +fun String.separateStringCoins() = this.split("_")[0] + +/** + * function that extends a string to get the + * second element separated by the '_' symbol + */ +fun String.getSecondCoinsText(): String { + val list = this.split("_") + return if (list.size >= 2) list[1] else this +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/base/CRYBaseActivity.kt b/app/src/main/java/com/javg/cryptocurrencies/view/base/CRYBaseActivity.kt new file mode 100644 index 000000000..eab17f853 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/base/CRYBaseActivity.kt @@ -0,0 +1,56 @@ +package com.javg.cryptocurrencies.view.base + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.google.android.material.snackbar.Snackbar +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.view.viewmodel.CRYAppViewModel +import kotlinx.coroutines.launch + +open class CRYBaseActivity : AppCompatActivity() { + private val appVM by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + testStateFlow() + } + + private fun testStateFlow() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + appVM.uiStateNetwork.collect { + if (it) { + showSuccessNetwork() + } else { + showErrorNetwork() + } + } + } + } + } + + private fun showSuccessNetwork() { + val snack = Snackbar.make( + this@CRYBaseActivity.findViewById(R.id.nav_host_main), + resources.getString(R.string.cry_success_network), + Snackbar.LENGTH_LONG, + ) + snack.setBackgroundTint(ContextCompat.getColor(this, R.color.green_success)) + snack.show() + } + + private fun showErrorNetwork() { + val snack = Snackbar.make( + this@CRYBaseActivity.findViewById(R.id.nav_host_main), + resources.getString(R.string.cry_error_network), + Snackbar.LENGTH_LONG, + ) + snack.setBackgroundTint(ContextCompat.getColor(this, R.color.red_error)) + snack.show() + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/book/CRYBookFragment.kt b/app/src/main/java/com/javg/cryptocurrencies/view/book/CRYBookFragment.kt new file mode 100644 index 000000000..ff5df8c25 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/book/CRYBookFragment.kt @@ -0,0 +1,210 @@ +package com.javg.cryptocurrencies.view.book + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.viewpager2.widget.CompositePageTransformer +import androidx.viewpager2.widget.MarginPageTransformer +import androidx.viewpager2.widget.ViewPager2 +import com.facebook.shimmer.ShimmerFrameLayout +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.data.model.CRYBook +import com.javg.cryptocurrencies.databinding.CryBookFragmentBinding +import com.javg.cryptocurrencies.view.book.recyclerview.CRYBookRecyclerView +import com.javg.cryptocurrencies.view.book.recyclerview.CRYChipsHeaderRecyclerView +import com.javg.cryptocurrencies.view.viewmodel.CRYHomeVM +import kotlin.math.abs + +/** + * @author Juan Vera Gomez + * Date modified 10/02/2023 + * + * Fragment in charge of manipulating and displaying the + * information corresponding to the cryptocurrency cards + * + * @since 2.2 + */ +class CRYBookFragment : Fragment() { + + private lateinit var binding: CryBookFragmentBinding + private lateinit var adapterBook: CRYBookRecyclerView + private lateinit var adapterChips: CRYChipsHeaderRecyclerView + private val bookHomeVM by activityViewModels() + private lateinit var shimmerFrameLayout: ShimmerFrameLayout + private lateinit var swipeRefreshLayout: SwipeRefreshLayout + + // Saves the Lambda event that is executed in the adapter item + private val onClickItem: (String, String) -> Unit = { book, image -> + val args = Bundle().apply { + putString("BOOK", book) + putString("IMAGE_NAME", image) + } + findNavController().navigate(R.id.action_cry_book_fragment_to_cry_detail_book_fragment, args) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = CryBookFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + loadShimmer() + loadAdapter() + loadViewPagerChips() + swipeRefresh() + bookHomeVM.getBooks() + bookHomeVM.updateTime.observe(viewLifecycleOwner, observerUpdateTime) + bookHomeVM.chipsTitles.observe(viewLifecycleOwner, observerBooksChips) + bookHomeVM.equalBooks.observe(viewLifecycleOwner, observerEqualsBooks) + bookHomeVM.result.observe(viewLifecycleOwner, observerResult) + } + + /** + * Performs an update when the user interacts with + * the view in order to update the books in the database + */ + private fun swipeRefresh() { + swipeRefreshLayout = binding.swipeRefreshLayout + swipeRefreshLayout.setOnRefreshListener { + binding.rvBooks.visibility = View.GONE + loadShimmer() + bookHomeVM.getBooks() + } + } + + /** + * It is responsible for linking the visual component + * and starting the facebook loading effect + */ + private fun loadShimmer() { + binding.idShimmer.visibility = View.VISIBLE + shimmerFrameLayout = binding.idShimmer + shimmerFrameLayout.startShimmer() + } + + /** + * Takes care of the adapter instance and the visual + * configuration of the recycle view + */ + private fun loadAdapter() { + adapterBook = CRYBookRecyclerView(requireContext(), onClickItem) + binding.rvBooks.apply { + setHasFixedSize(false) + layoutManager = LinearLayoutManager(context) + adapter = adapterBook + } + } + + /** + * Start the general configuration of the viewPager of the view + */ + private fun loadViewPagerChips() { + adapterChips = CRYChipsHeaderRecyclerView(requireContext()) + binding.includeChips.vpChips.adapter = adapterChips + setupViewPager() + onPagerListenerChips() + } + + /** + * Configure the viewPager to transform the size of the card that is selected + */ + private fun setupViewPager() { + with(binding) { + includeChips.vpChips.clipToPadding = false + includeChips.vpChips.clipChildren = false + includeChips.vpChips.offscreenPageLimit = 3 + includeChips.vpChips.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER + + val compositePageTransformer = CompositePageTransformer() + compositePageTransformer.addTransformer(MarginPageTransformer(40)) + compositePageTransformer.addTransformer { page, position -> + page.apply { + val r = 1 - abs(position) + scaleY = (0.20f + r + 0.15f) + } + } + includeChips.vpChips.setPageTransformer(compositePageTransformer) + // redireccionar a posicion especifica + /*includeChips.vpChips.post { + includeChips.vpChips.setCurrentItem(5,false) + }*/ + } + } + + /** + * Contains the functionality to be able to detect when the user + * has changed the page in the view + */ + private fun onPagerListenerChips() { + binding.includeChips.vpChips.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + println("onPageSelected -> $position") + bookHomeVM.updateListDifferentBook(position) + } + }) + } + + /** + * It is in charge of observing the list that contains the same + * books of the page of selection of the view + */ + private val observerEqualsBooks = Observer> { equalsBooks -> + equalsBooks?.let { + swipeRefreshLayout.isRefreshing = false + + if (equalsBooks.isEmpty()) { + Toast.makeText(requireContext(), "Lista vacia", Toast.LENGTH_SHORT).show() + } else { + shimmerFrameLayout.stopShimmer() + binding.idShimmer.visibility = View.GONE + binding.rvBooks.visibility = View.VISIBLE + adapterBook.submitList(equalsBooks) + } + } + } + + /** + * Look at the list that contains the unique books to + * be able to show the selection options + * + */ + private val observerBooksChips = Observer> { listBooks -> + listBooks?.let { + adapterChips.submitList(listBooks) + } + } + + /** + * Observes the time change and reflects it in the view + */ + private val observerUpdateTime = Observer { + it?.let { updateTime -> + binding.updateDay.text = updateTime + } + } + + private val observerResult = Observer { result -> + result?.let { + if (it) { + println("success") + } else { + println("error") + } + } + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/book/recyclerview/CRYBookRecyclerView.kt b/app/src/main/java/com/javg/cryptocurrencies/view/book/recyclerview/CRYBookRecyclerView.kt new file mode 100644 index 000000000..b9c195a2a --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/book/recyclerview/CRYBookRecyclerView.kt @@ -0,0 +1,68 @@ +package com.javg.cryptocurrencies.view.book.recyclerview + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.data.model.CRYBook +import com.javg.cryptocurrencies.databinding.CryBookItemBinding +import com.javg.cryptocurrencies.utils.getSecondCoinsText +import com.javg.cryptocurrencies.utils.separateStringCoins + +/** + * @author Juan Vera Gomez + * Date modified 08/02/2023 + * + * Contains the necessary functions for the adapter to + * display cryptocurrency books to work + * + * @since 2.0 + */ + +class CRYBookRecyclerView( + private val context: Context, + private val onItemClick: (String, String) -> Unit, +) : + ListAdapter(BookOrderDiffCallback()) { + + inner class CRYBookViewHolder(private val binding: CryBookItemBinding) : RecyclerView.ViewHolder(binding.root) { + + /** + * Function in charge of setting the information of the book model that it receives + */ + fun bind(book: CRYBook) { + binding.txtBook.text = book.book.separateStringCoins().uppercase() + binding.txtBookEnd.text = book.book.getSecondCoinsText().uppercase() + Glide.with(context) + .load(book.imageUrl) + .placeholder(R.drawable.ic_default_book) + .into(binding.imageId) + Glide.with(context) + .load(book.bookDestination) + .placeholder(R.drawable.ic_default_book) + .into(binding.imageIdEnd) + binding.imageMore.setOnClickListener { + onItemClick(book.book, book.imageUrl) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CRYBookViewHolder { + val binding: CryBookItemBinding = CryBookItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return CRYBookViewHolder(binding) + } + + override fun onBindViewHolder(holder: CRYBookViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class BookOrderDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CRYBook, newItem: CRYBook) = oldItem.book == newItem.book + + override fun areContentsTheSame(oldItem: CRYBook, newItem: CRYBook) = oldItem == newItem +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/book/recyclerview/CRYChipsHeaderRecyclerView.kt b/app/src/main/java/com/javg/cryptocurrencies/view/book/recyclerview/CRYChipsHeaderRecyclerView.kt new file mode 100644 index 000000000..9c1871b80 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/book/recyclerview/CRYChipsHeaderRecyclerView.kt @@ -0,0 +1,55 @@ +package com.javg.cryptocurrencies.view.book.recyclerview + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.data.model.CRYBook +import com.javg.cryptocurrencies.databinding.CryChipsItemBinding +import com.javg.cryptocurrencies.utils.separateStringCoins + +class CRYChipsHeaderRecyclerView(private val context: Context) : + ListAdapter(ChipsDiffCallback()) { + + inner class CRyChipsViewHolder(private val binding: CryChipsItemBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(book: CRYBook, position: Int) { + if (position != 0) { + binding.clContainer.setBackgroundColor(ContextCompat.getColor(context, R.color.transparent)) + } + + binding.txtTitleBook.text = book.book.separateStringCoins().uppercase() + Glide.with(context) + .load(book.imageUrl) + .placeholder(R.drawable.ic_default_book) + .into(binding.ivBook) + + binding.clContainer.setOnClickListener { + if (position == absoluteAdapterPosition) { + binding.clContainer.background = ContextCompat.getDrawable(context, R.drawable.background_button_oval) + } else { + binding.clContainer.setBackgroundColor(ContextCompat.getColor(context, R.color.transparent)) + } + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CRyChipsViewHolder { + val binding: CryChipsItemBinding = CryChipsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return CRyChipsViewHolder(binding) + } + + override fun onBindViewHolder(holder: CRyChipsViewHolder, position: Int) { + holder.bind(getItem(position), position) + } +} + +class ChipsDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CRYBook, newItem: CRYBook) = oldItem.book == newItem.book + + override fun areContentsTheSame(oldItem: CRYBook, newItem: CRYBook) = oldItem == newItem +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/detail/CRYDetailBookFragment.kt b/app/src/main/java/com/javg/cryptocurrencies/view/detail/CRYDetailBookFragment.kt new file mode 100644 index 000000000..4d3609b42 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/detail/CRYDetailBookFragment.kt @@ -0,0 +1,197 @@ +package com.javg.cryptocurrencies.view.detail + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.airbnb.lottie.LottieDrawable +import com.bumptech.glide.Glide +import com.facebook.shimmer.ShimmerFrameLayout +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.data.model.CRYAskOrBids +import com.javg.cryptocurrencies.data.model.CRYDetailBook +import com.javg.cryptocurrencies.databinding.CryDetailBookFragmentBinding +import com.javg.cryptocurrencies.utils.separateStringCoins +import com.javg.cryptocurrencies.view.detail.recyclerview.CRYAskRecyclerView +import com.javg.cryptocurrencies.view.viewmodel.CRYDetailBookVM + +/** + * @author Juan Antonio Vera + * Date modified 22/02/2023 + * + * They contain the necessary functionalities to show the detailed + * information of a specific book visually. + * + * @since 2.1 + */ +class CRYDetailBookFragment : Fragment() { + private lateinit var binding: CryDetailBookFragmentBinding + private val detailBookVM by activityViewModels() + private lateinit var adapterAskBids: CRYAskRecyclerView + private lateinit var shimmerFrameLayout: ShimmerFrameLayout + private val lottieAsset = "empty_data.json" + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = CryDetailBookFragmentBinding.inflate(inflater, container, false) + return binding.root + } + + /** + * the functionality is overwritten, in which you have + * the functions for the view and the data that is + * requested by instance is obtained + */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + val book = arguments?.getString("BOOK").orEmpty() + val imageId = arguments?.getString("IMAGE_NAME").orEmpty() + loadData(book, imageId) + onClickListener() + } + + /** + * It is in charge of displaying the information of the book in addition + * to displaying the image that is received, configuring the adapter + * and having the observers for when the information changes. + * + * @param book is the name of the book to show its details + * @param imageName is the name of the image that will be consulted online to display + */ + private fun loadData(book: String, imageName: String) { + if (book.isNotEmpty()) { + detailBookVM.getTicker(book) + } + + Glide.with(requireContext()) + .load(imageName) + .placeholder(R.drawable.ic_default_book) + .into(binding.bookImage) + binding.headerTopBar.title.text = book.separateStringCoins().uppercase() + configureAdapter() + + detailBookVM.tickerBook.observe(viewLifecycleOwner, tickerObserver) + detailBookVM.listAskOrBids.observe(viewLifecycleOwner, listAskOrBids) + detailBookVM.emptyData.observe(viewLifecycleOwner, emptyDataObserver) + detailBookVM.updateTime.observe(viewLifecycleOwner, observerUpdateTime) + } + + /** + * Configure and instantiate the adapter to display ask and bids + */ + private fun configureAdapter() { + adapterAskBids = CRYAskRecyclerView(requireContext()) + binding.rvAskBids.apply { + setHasFixedSize(false) + layoutManager = LinearLayoutManager(context) + adapter = adapterAskBids + } + } + + /** + * listens for the click events of the view when the user interacts + */ + private fun onClickListener() = with(binding) { + headerTopBar.imageBack.setOnClickListener { + findNavController().popBackStack() + } + txtAsk.setOnClickListener { changeAsk() } + txtBids.setOnClickListener { changeBids() } + } + + /** + * Control the visual configuration when selecting ask, + * changing the design and color of the text and background + */ + private fun changeAsk() { + binding.txtAsk.background = AppCompatResources.getDrawable(requireContext(), R.drawable.background_button_line) + binding.txtBids.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.transparent)) + detailBookVM.tickerBook.value?.askList?.let { detailBookVM.sendListUpdate(it) } + } + + /** + * Control the visual configuration when selecting bids, + * changing the design and color of the text and background + */ + private fun changeBids() { + binding.txtAsk.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.transparent)) + binding.txtBids.background = AppCompatResources.getDrawable(requireContext(), R.drawable.background_button_line) + detailBookVM.tickerBook.value?.bidsList?.let { detailBookVM.sendListUpdate(it) } + } + + private fun startAnimationNoData() { + binding.clNotInformation.visibility = View.VISIBLE + binding.lavNotInformation.imageAssetsFolder = "assets" + binding.lavNotInformation.setAnimation(lottieAsset) + binding.lavNotInformation.repeatCount = LottieDrawable.INFINITE + binding.lavNotInformation.playAnimation() + } + + private fun hideContentAll() { + binding.bookImage.visibility = View.GONE + binding.txtLastPrice.visibility = View.GONE + binding.edLastPrice.visibility = View.GONE + binding.clContainerAll.visibility = View.GONE + } + + private fun showContentAll() { + binding.bookImage.visibility = View.VISIBLE + binding.txtLastPrice.visibility = View.VISIBLE + binding.edLastPrice.visibility = View.VISIBLE + binding.clContainerAll.visibility = View.VISIBLE + binding.clNotInformation.visibility = View.GONE + } + + /** + * Observe when the data of the prices of the specific book changes + */ + private val tickerObserver = Observer { + it?.let { + with(binding) { + edLastPrice.text = String.format(requireContext().resources.getString(R.string.cry_format_amount), it.last) + inCardPrice.edHighestPrice.text = String.format(requireContext().resources.getString(R.string.cry_format_amount), it.high) + inCardPrice.edLowMorePrice.text = String.format(requireContext().resources.getString(R.string.cry_format_amount), it.low) + showContentAll() + } + } + } + + /** + * watches when the ask or bid list changes informing + * the adapter of the change, as well as the + * corresponding visual changes + */ + private val listAskOrBids = Observer> { + it?.let { + adapterAskBids.submitList(it) + } + } + + private val emptyDataObserver = Observer { + it?.let { + if (it) { + startAnimationNoData() + hideContentAll() + } + } + } + + /** + * Observes the time change and reflects it in the view + */ + private val observerUpdateTime = Observer { + it?.let { updateTime -> + binding.headerTopBar.txtLastUpdate.text = updateTime + } + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/detail/recyclerview/CRYAskRecyclerView.kt b/app/src/main/java/com/javg/cryptocurrencies/view/detail/recyclerview/CRYAskRecyclerView.kt new file mode 100644 index 000000000..8e91f07fb --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/detail/recyclerview/CRYAskRecyclerView.kt @@ -0,0 +1,52 @@ +package com.javg.cryptocurrencies.view.detail.recyclerview + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.data.model.CRYAskOrBids +import com.javg.cryptocurrencies.databinding.CryAskItemBinding + +/** + * @author Juan Vera Gomez + * Date modified 08/02/2023 + * + * Contains the necessary functions to display + * the ask and bids information + * + * @since 2.0 + */ +class CRYAskRecyclerView( + private val context: Context, +) : + ListAdapter(AskAndBindDiffCallback()) { + + inner class CRYAskViewHolder(private val binding: CryAskItemBinding) : RecyclerView.ViewHolder(binding.root) { + + /** + * Function in charge of setting the information of the ask or bids model that it receives + */ + fun bind(askOrBids: CRYAskOrBids) { + binding.edPrice.text = String.format(context.getString(R.string.cry_price_order), askOrBids.price) + binding.edAmount.text = askOrBids.amount + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CRYAskViewHolder { + val binding = CryAskItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return CRYAskViewHolder(binding) + } + + override fun onBindViewHolder(holder: CRYAskViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} + +class AskAndBindDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CRYAskOrBids, newItem: CRYAskOrBids) = oldItem.book == newItem.book + + override fun areContentsTheSame(oldItem: CRYAskOrBids, newItem: CRYAskOrBids) = oldItem == newItem +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/theme/Color.kt b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Color.kt new file mode 100644 index 000000000..c7590f978 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Color.kt @@ -0,0 +1,10 @@ +package com.javg.cryptocurrencies.view.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) + +val Blue300 = Color(0xFF0C7A96) \ No newline at end of file diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/theme/Shape.kt b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Shape.kt new file mode 100644 index 000000000..52c3744ae --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.javg.cryptocurrencies.view.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/theme/Theme.kt b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Theme.kt new file mode 100644 index 000000000..6e0f6b0a1 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Theme.kt @@ -0,0 +1,47 @@ +package com.javg.cryptocurrencies.view.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun CryptocurrenciesTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/theme/Type.kt b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Type.kt new file mode 100644 index 000000000..e214bd29e --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/theme/Type.kt @@ -0,0 +1,28 @@ +package com.javg.cryptocurrencies.view.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYAppViewModel.kt b/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYAppViewModel.kt new file mode 100644 index 000000000..9ba84d3fc --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYAppViewModel.kt @@ -0,0 +1,19 @@ +package com.javg.cryptocurrencies.view.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import com.javg.cryptocurrencies.utils.CRYUtils +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class CRYAppViewModel(application: Application) : AndroidViewModel(application) { + private val _uiStateNetwork = MutableStateFlow(true) + + val uiStateNetwork: StateFlow = _uiStateNetwork + + init { + CRYUtils.isInternetAvailable(application.applicationContext) { + _uiStateNetwork.value = it + } + } +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYDetailBookVM.kt b/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYDetailBookVM.kt new file mode 100644 index 000000000..519a8ad06 --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYDetailBookVM.kt @@ -0,0 +1,92 @@ +package com.javg.cryptocurrencies.view.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.data.domain.CRYGetListBookWithTickerUseCase +import com.javg.cryptocurrencies.data.model.CRYAskOrBids +import com.javg.cryptocurrencies.data.model.CRYDetailBook +import com.javg.cryptocurrencies.utils.CRYUtils +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * @author Juan Vera Gomez + * + * Contains the functionality to consult the specific information of each book + * + * @param tickerUseCase It is the case of use who manages + * the obtaining of information in the data layer + * + * @since 2.0 + */ +@HiltViewModel +class CRYDetailBookVM @Inject constructor( + application: Application, + private val tickerUseCase: CRYGetListBookWithTickerUseCase, +) : AndroidViewModel(application) { + + private var _tickerBook = MutableLiveData() + private var _listAskOrBids = MutableLiveData>() + private var _emptyData = MutableLiveData() + private var _updateTime = MutableLiveData() + + val tickerBook: LiveData + get() = _tickerBook + + val listAskOrBids: LiveData> + get() = _listAskOrBids + + val emptyData: LiveData + get() = _emptyData + + val updateTime: LiveData + get() = _updateTime + + init { + _updateTime.value = getTimeUpdate() + } + + /** + * Consult the price information and ask list and bids of a specific book + * + * @param book is the name of the book to consult its specific information + */ + fun getTicker(book: String) { + viewModelScope.launch { + val ticker = tickerUseCase.invoke(book) + ticker?.let { + _tickerBook.value = it + it.askList?.let { list -> + _listAskOrBids.value = list as MutableList + _updateTime.value = getTimeUpdate() + } + } ?: run { + _emptyData.value = true + } + } + } + + /** + * Updates a list depending on the selection at view level + * + * @param listUpdate is the list that will update the view + */ + fun sendListUpdate(listUpdate: List) { + _listAskOrBids.value = listUpdate + } + + /** + * Returns a composite legend with the updated time of the remote service query + */ + private fun getTimeUpdate(): String = String.format( + getApplication().applicationContext.getString( + R.string.cry_update_day, + ), + CRYUtils.getSaveTime(getApplication().applicationContext), + ) +} diff --git a/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYHomeVM.kt b/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYHomeVM.kt new file mode 100644 index 000000000..af5386dfb --- /dev/null +++ b/app/src/main/java/com/javg/cryptocurrencies/view/viewmodel/CRYHomeVM.kt @@ -0,0 +1,98 @@ +package com.javg.cryptocurrencies.view.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import com.javg.cryptocurrencies.R +import com.javg.cryptocurrencies.data.domain.CRYBookUseCase +import com.javg.cryptocurrencies.data.model.CRYBook +import com.javg.cryptocurrencies.utils.CRYUtils +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +/** + * @author Juan Vera Gomez + * Date modified 22/02/2023 + * + * Contains the necessary functions to obtain the information + * from the cryptocurrency books + * + * @since 1.3 + */ +@HiltViewModel +class CRYHomeVM @Inject constructor( + application: Application, + private val bookUseCase: CRYBookUseCase, +) : AndroidViewModel(application) { + + private var _booksMap = MutableLiveData>>() + private var _books = MutableLiveData>() + private var _updateTime = MutableLiveData() + private var _chipsTitles = MutableLiveData>() + private var _equalBooks = MutableLiveData>() + private var _result = MutableLiveData() + + val updateTime: LiveData + get() = _updateTime + + val chipsTitles: LiveData> + get() = _chipsTitles + + val equalBooks: LiveData> + get() = _equalBooks + + val result: LiveData + get() = _result + + init { + _updateTime.value = getTimeUpdate() + queryBookFlow() + } + + /** + * It is responsible for requesting the list of books from the data + * layer which can be called with user interaction. + * + */ + fun getBooks() { + val result = bookUseCase.getAvailableBooksRx() + _result.postValue(result) + } + + /** + * Updates the view list with another list according to the + * position selected by the user + * + * @param position is the position of the user selection + */ + fun updateListDifferentBook(position: Int) { + val book = _chipsTitles.value?.get(position) + _equalBooks.value = _booksMap.value?.get(book?.singleBook) + } + + /** + * Returns a composite legend with the updated time of the remote service query + */ + private fun getTimeUpdate(): String = String.format(getApplication().applicationContext.getString(R.string.cry_update_day), CRYUtils.getSaveTime(getApplication().applicationContext)) + + /** + * It is in charge of observing the changes of the list of books + * in the database and in case it has changed, it refreshes the view + * with the new information stored in it. + */ + private fun queryBookFlow() { + viewModelScope.launch { + bookUseCase.queryBooks().collect { + if (it.isNotEmpty()) { + _books.value = bookUseCase.transformBooks(it) + _chipsTitles.value = bookUseCase.createListBookTitles(_books.value!!) + _booksMap.value = bookUseCase.createUniqueMap(_books.value!!) + _equalBooks.value = _booksMap.value?.get(_chipsTitles.value?.firstOrNull()?.singleBook) + } + } + } + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_button_line.xml b/app/src/main/res/drawable/background_button_line.xml new file mode 100644 index 000000000..e23a78f47 --- /dev/null +++ b/app/src/main/res/drawable/background_button_line.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_button_oval.xml b/app/src/main/res/drawable/background_button_oval.xml new file mode 100644 index 000000000..973225add --- /dev/null +++ b/app/src/main/res/drawable/background_button_oval.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_circle.xml b/app/src/main/res/drawable/background_circle.xml new file mode 100644 index 000000000..e5e80db9f --- /dev/null +++ b/app/src/main/res/drawable/background_circle.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_header_rounded.xml b/app/src/main/res/drawable/background_header_rounded.xml new file mode 100644 index 000000000..ebe9ff465 --- /dev/null +++ b/app/src/main/res/drawable/background_header_rounded.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/background_rounded_corner.xml b/app/src/main/res/drawable/background_rounded_corner.xml new file mode 100644 index 000000000..686d35c31 --- /dev/null +++ b/app/src/main/res/drawable/background_rounded_corner.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_aave.xml b/app/src/main/res/drawable/ic_aave.xml new file mode 100644 index 000000000..a74b65588 --- /dev/null +++ b/app/src/main/res/drawable/ic_aave.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_down.xml b/app/src/main/res/drawable/ic_arrow_down.xml new file mode 100644 index 000000000..dcfb7840c --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_down.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_left.xml b/app/src/main/res/drawable/ic_arrow_left.xml new file mode 100644 index 000000000..3514225eb --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_left.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_arrow_right.xml b/app/src/main/res/drawable/ic_arrow_right.xml new file mode 100644 index 000000000..5222707e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_right.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 000000000..46d5d587d --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_avalanche.xml b/app/src/main/res/drawable/ic_avalanche.xml new file mode 100644 index 000000000..92080991e --- /dev/null +++ b/app/src/main/res/drawable/ic_avalanche.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml new file mode 100644 index 000000000..f4d35e884 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_binance.xml b/app/src/main/res/drawable/ic_binance.xml new file mode 100644 index 000000000..6f409da39 --- /dev/null +++ b/app/src/main/res/drawable/ic_binance.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_bitcoin.xml b/app/src/main/res/drawable/ic_bitcoin.xml new file mode 100644 index 000000000..bc5a4ae08 --- /dev/null +++ b/app/src/main/res/drawable/ic_bitcoin.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_bitcoin_cash.xml b/app/src/main/res/drawable/ic_bitcoin_cash.xml new file mode 100644 index 000000000..1c4306433 --- /dev/null +++ b/app/src/main/res/drawable/ic_bitcoin_cash.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_cardano.xml b/app/src/main/res/drawable/ic_cardano.xml new file mode 100644 index 000000000..e014cdb55 --- /dev/null +++ b/app/src/main/res/drawable/ic_cardano.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_chainlink.xml b/app/src/main/res/drawable/ic_chainlink.xml new file mode 100644 index 000000000..1682c9783 --- /dev/null +++ b/app/src/main/res/drawable/ic_chainlink.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_circle.xml b/app/src/main/res/drawable/ic_circle.xml new file mode 100644 index 000000000..3dc5f0338 --- /dev/null +++ b/app/src/main/res/drawable/ic_circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 000000000..98d97ad7b --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_compound.xml b/app/src/main/res/drawable/ic_compound.xml new file mode 100644 index 000000000..26af67c82 --- /dev/null +++ b/app/src/main/res/drawable/ic_compound.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dai.xml b/app/src/main/res/drawable/ic_dai.xml new file mode 100644 index 000000000..01180ab57 --- /dev/null +++ b/app/src/main/res/drawable/ic_dai.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_dash.xml b/app/src/main/res/drawable/ic_dash.xml new file mode 100644 index 000000000..b7aba599a --- /dev/null +++ b/app/src/main/res/drawable/ic_dash.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_default_book.xml b/app/src/main/res/drawable/ic_default_book.xml new file mode 100644 index 000000000..815159ffa --- /dev/null +++ b/app/src/main/res/drawable/ic_default_book.xml @@ -0,0 +1,730 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_dfinity.xml b/app/src/main/res/drawable/ic_dfinity.xml new file mode 100644 index 000000000..5a16557fb --- /dev/null +++ b/app/src/main/res/drawable/ic_dfinity.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_dogecoin.xml b/app/src/main/res/drawable/ic_dogecoin.xml new file mode 100644 index 000000000..c78168ce7 --- /dev/null +++ b/app/src/main/res/drawable/ic_dogecoin.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_eos.xml b/app/src/main/res/drawable/ic_eos.xml new file mode 100644 index 000000000..88c5d568d --- /dev/null +++ b/app/src/main/res/drawable/ic_eos.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_ethereum.xml b/app/src/main/res/drawable/ic_ethereum.xml new file mode 100644 index 000000000..a75d0f266 --- /dev/null +++ b/app/src/main/res/drawable/ic_ethereum.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_filecoin.xml b/app/src/main/res/drawable/ic_filecoin.xml new file mode 100644 index 000000000..d84d776e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_filecoin.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_iota.xml b/app/src/main/res/drawable/ic_iota.xml new file mode 100644 index 000000000..2fdd48dcf --- /dev/null +++ b/app/src/main/res/drawable/ic_iota.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..c2cfce6f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_litecoin.xml b/app/src/main/res/drawable/ic_litecoin.xml new file mode 100644 index 000000000..938c758bd --- /dev/null +++ b/app/src/main/res/drawable/ic_litecoin.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_monero.xml b/app/src/main/res/drawable/ic_monero.xml new file mode 100644 index 000000000..f77d7adf8 --- /dev/null +++ b/app/src/main/res/drawable/ic_monero.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_more_circle.xml b/app/src/main/res/drawable/ic_more_circle.xml new file mode 100644 index 000000000..c5390aa94 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_circle.xml @@ -0,0 +1,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_nem.xml b/app/src/main/res/drawable/ic_nem.xml new file mode 100644 index 000000000..e4b6a610a --- /dev/null +++ b/app/src/main/res/drawable/ic_nem.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_neocoin.xml b/app/src/main/res/drawable/ic_neocoin.xml new file mode 100644 index 000000000..84b73f106 --- /dev/null +++ b/app/src/main/res/drawable/ic_neocoin.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_pancakeswap.xml b/app/src/main/res/drawable/ic_pancakeswap.xml new file mode 100644 index 000000000..ba7f77c70 --- /dev/null +++ b/app/src/main/res/drawable/ic_pancakeswap.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_polkadot.xml b/app/src/main/res/drawable/ic_polkadot.xml new file mode 100644 index 000000000..5499ca0ae --- /dev/null +++ b/app/src/main/res/drawable/ic_polkadot.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_polygon.xml b/app/src/main/res/drawable/ic_polygon.xml new file mode 100644 index 000000000..341096bae --- /dev/null +++ b/app/src/main/res/drawable/ic_polygon.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_right_double_arrow.xml b/app/src/main/res/drawable/ic_right_double_arrow.xml new file mode 100644 index 000000000..e733f34c6 --- /dev/null +++ b/app/src/main/res/drawable/ic_right_double_arrow.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_shiba.xml b/app/src/main/res/drawable/ic_shiba.xml new file mode 100644 index 000000000..94c5d0f52 --- /dev/null +++ b/app/src/main/res/drawable/ic_shiba.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_solana.xml b/app/src/main/res/drawable/ic_solana.xml new file mode 100644 index 000000000..e4f1848c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_solana.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_stellar.xml b/app/src/main/res/drawable/ic_stellar.xml new file mode 100644 index 000000000..73a449b09 --- /dev/null +++ b/app/src/main/res/drawable/ic_stellar.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_terra.xml b/app/src/main/res/drawable/ic_terra.xml new file mode 100644 index 000000000..86dd50160 --- /dev/null +++ b/app/src/main/res/drawable/ic_terra.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_tether.xml b/app/src/main/res/drawable/ic_tether.xml new file mode 100644 index 000000000..ee6effe0f --- /dev/null +++ b/app/src/main/res/drawable/ic_tether.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_theta.xml b/app/src/main/res/drawable/ic_theta.xml new file mode 100644 index 000000000..1c6a9abbf --- /dev/null +++ b/app/src/main/res/drawable/ic_theta.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_tron.xml b/app/src/main/res/drawable/ic_tron.xml new file mode 100644 index 000000000..f2126aa73 --- /dev/null +++ b/app/src/main/res/drawable/ic_tron.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_uniswap.xml b/app/src/main/res/drawable/ic_uniswap.xml new file mode 100644 index 000000000..bc0ad65fd --- /dev/null +++ b/app/src/main/res/drawable/ic_uniswap.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_vechain.xml b/app/src/main/res/drawable/ic_vechain.xml new file mode 100644 index 000000000..592ade125 --- /dev/null +++ b/app/src/main/res/drawable/ic_vechain.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_wrapped_bitcoin.xml b/app/src/main/res/drawable/ic_wrapped_bitcoin.xml new file mode 100644 index 000000000..5bec297b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_wrapped_bitcoin.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_xrp.xml b/app/src/main/res/drawable/ic_xrp.xml new file mode 100644 index 000000000..34096e5d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_xrp.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_zcash.xml b/app/src/main/res/drawable/ic_zcash.xml new file mode 100644 index 000000000..ae3962bde --- /dev/null +++ b/app/src/main/res/drawable/ic_zcash.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/logoapp2.xml b/app/src/main/res/drawable/logoapp2.xml new file mode 100644 index 000000000..44e55cf12 --- /dev/null +++ b/app/src/main/res/drawable/logoapp2.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/splash_screen.xml b/app/src/main/res/drawable/splash_screen.xml new file mode 100644 index 000000000..884a54955 --- /dev/null +++ b/app/src/main/res/drawable/splash_screen.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_activity.xml b/app/src/main/res/layout/cry_activity.xml new file mode 100644 index 000000000..2c0e215ea --- /dev/null +++ b/app/src/main/res/layout/cry_activity.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_ask_item.xml b/app/src/main/res/layout/cry_ask_item.xml new file mode 100644 index 000000000..0171176f8 --- /dev/null +++ b/app/src/main/res/layout/cry_ask_item.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_book_fragment.xml b/app/src/main/res/layout/cry_book_fragment.xml new file mode 100644 index 000000000..74241375a --- /dev/null +++ b/app/src/main/res/layout/cry_book_fragment.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_book_item.xml b/app/src/main/res/layout/cry_book_item.xml new file mode 100644 index 000000000..b95c84770 --- /dev/null +++ b/app/src/main/res/layout/cry_book_item.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_card_price_include.xml b/app/src/main/res/layout/cry_card_price_include.xml new file mode 100644 index 000000000..8b68d27f7 --- /dev/null +++ b/app/src/main/res/layout/cry_card_price_include.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_chips_item.xml b/app/src/main/res/layout/cry_chips_item.xml new file mode 100644 index 000000000..416ae16df --- /dev/null +++ b/app/src/main/res/layout/cry_chips_item.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_detail_book_fragment.xml b/app/src/main/res/layout/cry_detail_book_fragment.xml new file mode 100644 index 000000000..767cbc342 --- /dev/null +++ b/app/src/main/res/layout/cry_detail_book_fragment.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_header_book_include.xml b/app/src/main/res/layout/cry_header_book_include.xml new file mode 100644 index 000000000..f8e517362 --- /dev/null +++ b/app/src/main/res/layout/cry_header_book_include.xml @@ -0,0 +1,35 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_header_top_bar_include.xml b/app/src/main/res/layout/cry_header_top_bar_include.xml new file mode 100644 index 000000000..e7f081cb8 --- /dev/null +++ b/app/src/main/res/layout/cry_header_top_bar_include.xml @@ -0,0 +1,52 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_nav_host_main.xml b/app/src/main/res/layout/cry_nav_host_main.xml new file mode 100644 index 000000000..cd9d1142e --- /dev/null +++ b/app/src/main/res/layout/cry_nav_host_main.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_shimmer_detail_book.xml b/app/src/main/res/layout/cry_shimmer_detail_book.xml new file mode 100644 index 000000000..cf53ea09e --- /dev/null +++ b/app/src/main/res/layout/cry_shimmer_detail_book.xml @@ -0,0 +1,349 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cry_shimmer_layout.xml b/app/src/main/res/layout/cry_shimmer_layout.xml new file mode 100644 index 000000000..03eae1cb2 --- /dev/null +++ b/app/src/main/res/layout/cry_shimmer_layout.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..036d09bc5 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..e4810067f Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..82def3db1 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..80ff445d5 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..a09cffdcc Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..3171cae61 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..16a281cea Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..e6e0ed3e0 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..2043a252d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..9a9f53d49 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..04c639f72 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..81068d787 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..a55493399 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..696565876 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..b0b9312c8 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..0613ac42b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/navigation/cry_nav_graph_main.xml b/app/src/main/res/navigation/cry_nav_graph_main.xml new file mode 100644 index 000000000..044dc0385 --- /dev/null +++ b/app/src/main/res/navigation/cry_nav_graph_main.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..a46c0ce15 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,31 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + #BFBDBD + #918F8F + #726B6B + #0189ff + #286396 + #F6A700 + #e28000 + #e2504c + #ffd792 + + #0AB10A + #B00A0A + + #075366 + #00303c + #f3f3f3 + #bac7d8 + #3db5e6 + #00FFFFFF + #0C7A96 + #BFEB87 + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000..c5702b42e --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,40 @@ + + + 10sp + 12sp + 16sp + 18sp + 20sp + 22sp + 24sp + 32sp + 34sp + 36sp + 38sp + 1dp + 2dp + 4dp + 6dp + 8dp + 10dp + 12dp + 14dp + 16dp + 20dp + 22dp + 24dp + 28dp + 32dp + 34dp + 36dp + 38dp + 40dp + 50dp + 64dp + 84dp + 100dp + 150dp + 260dp + 300dp + 400dp + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..9f804ca0a --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #F6A700 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..f70ad1d86 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,23 @@ + + CryptoBook + BTC + MXN + Monto minimo: $.003 + Monto maximo: $1000.00 + Precio maximo: $1000000.00 + Ultimo precio + Más bajo + Más alto + Precio + Monto: + Demanda + Ofertas + $%1$s + Ultima actualización\n %1$s + Hay conexión a internet + Revisa tu conexión a internet + $%1$s + Detalle de la criptomoneda + Por el momento no hay información + $410060.00 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..0ba489db2 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 000000000..fa0f996d2 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..9ee9997b0 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/javg/cryptocurrencies/ExampleUnitTest.kt b/app/src/test/java/com/javg/cryptocurrencies/ExampleUnitTest.kt new file mode 100644 index 000000000..45e08c206 --- /dev/null +++ b/app/src/test/java/com/javg/cryptocurrencies/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.javg.cryptocurrencies + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/javg/cryptocurrencies/data/mapper/CRYMapperKtTest.kt b/app/src/test/java/com/javg/cryptocurrencies/data/mapper/CRYMapperKtTest.kt new file mode 100644 index 000000000..18b3a4e8d --- /dev/null +++ b/app/src/test/java/com/javg/cryptocurrencies/data/mapper/CRYMapperKtTest.kt @@ -0,0 +1,35 @@ +package com.javg.cryptocurrencies.data.mapper + +import com.javg.cryptocurrencies.data.model.CRYBookResponse +import com.google.common.truth.Truth.assertThat +import com.javg.cryptocurrencies.data.db.entity.CRYBookEntity +import com.javg.cryptocurrencies.data.model.CRYBook +import org.junit.Test + +class CRYMapperKtTest { + + @Test + fun toEntity() { + val target = CRYBookResponse( + book = "btc_mxn", + minimumPrice = ".003", + maximumPrice = "1000.00", + minimumAmount = "100.00", + maximumAmount = "1000000.00", + minimumValue = "25.00", + maximumValue = "1000000.00", + tickSize = "10", + defaultChart = "tradingview") + assertThat(target.toEntity()).isInstanceOf(CRYBookEntity::class.java) + } + + @Test + fun toDomain() { + val target = CRYBookEntity( + book = "btc_mxn", + minimumPrice = ".003", + maximumPrice = "1000.00") + + assertThat(target.toDomain()).isInstanceOf(CRYBook::class.java) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/javg/cryptocurrencies/data/model/CRYBookTest.kt b/app/src/test/java/com/javg/cryptocurrencies/data/model/CRYBookTest.kt new file mode 100644 index 000000000..615d1db06 --- /dev/null +++ b/app/src/test/java/com/javg/cryptocurrencies/data/model/CRYBookTest.kt @@ -0,0 +1,234 @@ +package com.javg.cryptocurrencies.data.model + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class CRYBookTest{ + private val target = CRYBook( + book = "btc_mxn", + imageUrl = "https://cryptoicons.org/api/icon/dai/200", + bookDestination = "https://cryptoicons.org/api/icon/dai/200") + private val target2 = CRYDetailBook() + private val target3 = CRYAskOrBids( + book = "btc_mxn", + price = "123", + amount = "123456") + + @Test + fun `test cryBook with custom properties`(){ + assertThat(target.book).isNotEmpty() + assertThat(target.imageUrl).isNotEmpty() + assertThat(target.bookDestination).isNotEmpty() + } + + @Test + fun `test cryBook with empty properties`(){ + target.apply { + book = "" + imageUrl = "" + bookDestination = "" + } + assertThat(target.book).isEmpty() + assertThat(target.imageUrl).isEmpty() + assertThat(target.bookDestination).isEmpty() + } + + @Test + fun `test cryDetailBook default properties`(){ + assertThat(target2.high).isEmpty() + assertThat(target2.low).isEmpty() + assertThat(target2.last).isEmpty() + assertThat(target2.askList).isNull() + assertThat(target2.bidsList).isNull() + } + + @Test + fun `test cryDetailBook with a custom property`(){ + target2.apply { + high = "1234" + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isEmpty() + assertThat(target2.last).isEmpty() + assertThat(target2.askList).isNull() + assertThat(target2.bidsList).isNull() + } + + @Test + fun `test cryDetailBook with two custom property`(){ + target2.apply { + high = "1234" + low = "123455667" + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isNotEmpty() + assertThat(target2.last).isEmpty() + assertThat(target2.askList).isNull() + assertThat(target2.bidsList).isNull() + } + + @Test + fun `test cryDetailBook with three custom property`(){ + target2.apply { + high = "1234" + low = "123455667" + last = "123456789" + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isNotEmpty() + assertThat(target2.last).isNotEmpty() + assertThat(target2.askList).isNull() + assertThat(target2.bidsList).isNull() + } + + @Test + fun `test cryDetailBook with empty list ask`(){ + target2.apply { + high = "1234" + low = "123455667" + last = "123456789" + askList = mutableListOf() + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isNotEmpty() + assertThat(target2.last).isNotEmpty() + assertThat(target2.askList).isEmpty() + assertThat(target2.bidsList).isNull() + } + + @Test + fun `test cryDetailBook with empty list bids`(){ + target2.apply { + high = "1234" + low = "123455667" + last = "123456789" + bidsList = mutableListOf() + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isNotEmpty() + assertThat(target2.last).isNotEmpty() + assertThat(target2.askList).isNull() + assertThat(target2.bidsList).isEmpty() + } + + @Test + fun `test cryDetailBook with list ask`(){ + target2.apply { + high = "1234" + low = "123455667" + last = "123456789" + askList = mutableListOf().apply { + add(CRYAskOrBids( + book = "12345", + price = "123", + amount = "1234567")) + } + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isNotEmpty() + assertThat(target2.last).isNotEmpty() + assertThat(target2.askList).isNotEmpty() + assertThat(target2.bidsList).isNull() + } + + @Test + fun `test cryDetailBook with list bids`(){ + target2.apply { + high = "1234" + low = "123455667" + last = "123456789" + bidsList = mutableListOf().apply { + add(CRYAskOrBids( + book = "12345", + price = "123", + amount = "1234567")) + } + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isNotEmpty() + assertThat(target2.last).isNotEmpty() + assertThat(target2.askList).isNull() + assertThat(target2.bidsList).isNotEmpty() + } + + @Test + fun `test cryDetailBook custom all properties`(){ + target2.apply { + high = "1234" + low = "123455667" + last = "123456789" + askList = mutableListOf().apply { + add(CRYAskOrBids( + book = "12345", + price = "123", + amount = "1234567")) + } + bidsList = mutableListOf().apply { + add(CRYAskOrBids( + book = "12345", + price = "123", + amount = "1234567")) + } + } + assertThat(target2.high).isNotEmpty() + assertThat(target2.low).isNotEmpty() + assertThat(target2.last).isNotEmpty() + assertThat(target2.askList).isNotEmpty() + assertThat(target2.bidsList).isNotEmpty() + } + + @Test + fun `test cryAskOrBids with custom properties`(){ + assertThat(target3.book).isNotEmpty() + assertThat(target3.price).isNotEmpty() + assertThat(target3.amount).isNotEmpty() + } + + @Test + fun `test cryAskOrBids with empty properties`(){ + target3.apply { + book = "" + price = "" + amount = "" + } + assertThat(target3.book).isEmpty() + assertThat(target3.price).isEmpty() + assertThat(target3.amount).isEmpty() + } + + @Test + fun `test cryAskOrBids with empty book property`(){ + target3.apply { + book = "" + price = "123" + amount = "123456789" + } + assertThat(target3.book).isEmpty() + assertThat(target3.price).isNotEmpty() + assertThat(target3.amount).isNotEmpty() + } + + @Test + fun `test cryAskOrBids with empty price property`(){ + target3.apply { + book = "btc_mxn" + price = "" + amount = "123456789" + } + assertThat(target3.book).isNotEmpty() + assertThat(target3.price).isEmpty() + assertThat(target3.amount).isNotEmpty() + } + + @Test + fun `test cryAskOrBids with empty amount property`(){ + target3.apply { + book = "btc_mxn" + price = "1234" + amount = "" + } + assertThat(target3.book).isNotEmpty() + assertThat(target3.price).isNotEmpty() + assertThat(target3.amount).isEmpty() + } +} \ No newline at end of file diff --git a/app/src/test/java/com/javg/cryptocurrencies/ext/CryStringExtKtTest.kt b/app/src/test/java/com/javg/cryptocurrencies/ext/CryStringExtKtTest.kt new file mode 100644 index 000000000..37c212cf5 --- /dev/null +++ b/app/src/test/java/com/javg/cryptocurrencies/ext/CryStringExtKtTest.kt @@ -0,0 +1,49 @@ +package com.javg.cryptocurrencies.ext + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class CryStringExtKtTest{ + + @Test + fun `test to separate string with symbol _ and get the first text`(){ + val text = "btc_mxn".separateStringCoins() + assertThat(text).isEqualTo("btc") + } + + @Test + fun `test to separate string without symbol _ and get the first text`(){ + val text = "btc mxn".separateStringCoins() + assertThat(text).isEqualTo("btc mxn") + } + + @Test + fun `test to separate empty string and get the first text`(){ + val text = "".separateStringCoins() + assertThat(text).isEqualTo("") + } + + @Test + fun `test to separate string with a symbol other than _`(){ + val text = "btc%mxn".separateStringCoins() + assertThat(text).isEqualTo("btc%mxn") + } + + @Test + fun `test to get the second text separated by the _ symbol`(){ + val text = "btc_mxn".getSecondCoinsText() + assertThat(text).isEqualTo("mxn") + } + + @Test + fun `test to get the second separated text without the _ symbol`(){ + val text = "btc mxn".getSecondCoinsText() + assertThat(text).isEqualTo("btc mxn") + } + + @Test + fun `test to get the second separated text`(){ + val text = "btc".getSecondCoinsText() + assertThat(text).isEqualTo("btc") + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..6a5610e35 --- /dev/null +++ b/build.gradle @@ -0,0 +1,43 @@ +buildscript { + dependencies { + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.0" + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } + ext { + compose_ui_version = '1.2.0' + room_version = '2.5.0' + googleMaterialVersion = '1.4.0' + retrofit_version = '2.9.0' + interceptor_version = '4.9.3' + hilt_version = '2.42' + hilt_compiler_version = '1.0.0' + lifecycle_version = '2.5.0' + runtime_livedata_version = '1.1.1' + constraintlayout_version = '2.2.0-alpha05' + constraintlayout_compose_version = '1.1.0-alpha05' + recyclerview_version = '1.2.1' + recyclerview_selection_version = '1.1.0' + fragment_ktx_version = '1.3.2' + fragment_version = '1.3.5' + appcompat_version = '1.2.0' + legacy_support_version = '1.0.0' + activity_ktx_version = '1.2.3' + cardview_version = '1.0.0' + glide_version = '4.13.0' + lottie_version = '3.4.0' + viewpager2_version = '1.0.0' + navigation_fragment_version = '2.5.3' + rx_android_version = '1.2.1' + rx_java_version = '1.2.7' + rxjava2_java_version = '2.2.8' + rxjava2_android_version = '2.1.1' + } +}// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id 'com.android.application' version '7.4.0' apply false + id 'com.android.library' version '7.4.0' apply false + id 'org.jetbrains.kotlin.android' version '1.7.0' apply false + id 'com.google.dagger.hilt.android' version '2.42' apply false + //id 'org.jlleitschuh.gradle:ktlint-gradle' version '10.2.0' apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..3c5031eb7 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..e708b1c02 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..9a712e57f --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Jan 24 12:29:26 CST 2023 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..ac1b06f93 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..31df13750 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,16 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "Cryptocurrencies" +include ':app'