From 4b971ac72b8da9edf138d7c773a51e9b23132b3b Mon Sep 17 00:00:00 2001 From: Dmytro Ponomarenko Date: Sun, 21 Jan 2024 15:24:35 +0200 Subject: [PATCH 01/58] Updated gradle plugin 8.5, kotlin 1.9.22, java v17, target SDK 34 --- app/build.gradle | 16 ++++++++++------ build.gradle | 4 ++-- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index eddab7c2f..4a40b835a 100755 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,14 +6,18 @@ apply plugin: 'kotlin-kapt' //apply plugin: 'com.google.gms.google-services' android { + + lint { + abortOnError false + } namespace 'com.dimowner.audiorecorder' - compileSdkVersion 33 + compileSdkVersion 34 defaultConfig { applicationId "com.dimowner.audiorecorder" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 934 - versionName "0.9.33-230715" + versionName "0.9.34" } buildFeatures { @@ -69,8 +73,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } lintOptions { @@ -91,7 +95,7 @@ android.variantFilter { variant -> } dependencies { - def androidX = "1.3.0" + def androidX = "1.3.2" def coroutines = "1.6.4" def timber = "5.0.1" diff --git a/build.gradle b/build.gradle index 2c6b3e799..20f6cf264 100755 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.8.0' + ext.kotlin_version = '1.9.22' repositories { google() @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // classpath 'com.google.gms:google-services:4.3.10' diff --git a/gradle.properties b/gradle.properties index 410a64313..c097316a8 100755 --- a/gradle.properties +++ b/gradle.properties @@ -13,6 +13,7 @@ org.gradle.jvmargs=-Xmx1536m #android.debug.obsoleteApi=true android.useAndroidX=true android.enableJetifier=true +android.defaults.buildfeatures.buildconfig=true # 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index eecd535c0..620019c35 100755 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip From 19c03168764d66e4b0ac0f481986d646f884312c Mon Sep 17 00:00:00 2001 From: Dmytro Ponomarenko Date: Sun, 21 Jan 2024 20:58:18 +0200 Subject: [PATCH 02/58] Migrated to gradle.ktx, version catalogs and ksp. --- app/build.gradle | 119 ------------------ app/build.gradle.kts | 97 ++++++++++++++ .../app/browser/FileBrowserAdapter.java | 2 +- .../app/lostrecords/LostRecordsAdapter.java | 2 +- .../app/moverecords/MoveRecordsAdapter.kt | 6 +- .../app/records/RecordsAdapter.java | 8 +- .../audiorecorder/app/trash/TrashAdapter.java | 2 +- build.gradle | 27 ---- build.gradle.kts | 15 +++ gradle.properties | 8 +- gradle/libs.versions.toml | 47 +++++++ settings.gradle | 1 - settings.gradle.kts | 18 +++ 13 files changed, 194 insertions(+), 158 deletions(-) delete mode 100755 app/build.gradle create mode 100644 app/build.gradle.kts delete mode 100755 build.gradle create mode 100644 build.gradle.kts create mode 100644 gradle/libs.versions.toml delete mode 100755 settings.gradle create mode 100644 settings.gradle.kts diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100755 index 4a40b835a..000000000 --- a/app/build.gradle +++ /dev/null @@ -1,119 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-parcelize' -apply plugin: 'kotlin-kapt' -//apply plugin: 'com.google.firebase.crashlytics' -//apply plugin: 'com.google.gms.google-services' - -android { - - lint { - abortOnError false - } - namespace 'com.dimowner.audiorecorder' - compileSdkVersion 34 - defaultConfig { - applicationId "com.dimowner.audiorecorder" - minSdkVersion 21 - targetSdkVersion 34 - versionCode 934 - versionName "0.9.34" - } - - buildFeatures { - viewBinding true - } - - def keystorePropertiesFile = rootProject.file("keystore.properties") - def keystoreProperties = new Properties() - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) - - signingConfigs { - dev { - storeFile file('key/debug/debug.keystore') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' - } - release { - storeFile file(keystoreProperties['prodStoreFile']) - storePassword keystoreProperties['prodStorePassword'] - keyAlias keystoreProperties['prodKeyAlias'] - keyPassword keystoreProperties['prodKeyPassword'] - } - } - - buildTypes { - release { - minifyEnabled true - shrinkResources true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' -// firebaseCrashlytics { -// mappingFileUploadEnabled true -// } - } - debug { - minifyEnabled false - } - } - - flavorDimensions "default" - - productFlavors { - debugConfig { - dimension "default" - applicationId "com.dimowner.audiorecorder.debug" - signingConfig = signingConfigs.dev - } - releaseConfig { - dimension "default" - signingConfig = signingConfigs.dev - applicationId "com.dimowner.audiorecorder" - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - lintOptions { - abortOnError false - } -} - -// Remove not needed buildVariants. -android.variantFilter { variant -> - if (variant.buildType.name == 'release' - && variant.getFlavors().get(0).name == 'debugConfig') { - variant.setIgnore(true) - } - if (variant.buildType.name == 'debug' - && variant.getFlavors().get(0).name == 'releaseConfig') { - variant.setIgnore(true) - } -} - -dependencies { - def androidX = "1.3.2" - def coroutines = "1.6.4" - def timber = "5.0.1" - - //Kotlin - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines" - - //Timber - implementation "com.jakewharton.timber:timber:$timber" - implementation "androidx.recyclerview:recyclerview:$androidX" - - implementation "androidx.viewpager2:viewpager2:1.0.0" - -// // Import the BoM for the Firebase platform -// implementation platform('com.google.firebase:firebase-bom:26.1.0') -// // Declare the dependencies for the Crashlytics and Analytics libraries -// // When using the BoM, you don't specify versions in Firebase library dependencies -// implementation 'com.google.firebase:firebase-crashlytics' -// implementation 'com.google.firebase:firebase-analytics' -} diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 000000000..f16057c24 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,97 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.ksp) + alias(libs.plugins.hilt) + id("kotlin-parcelize") +} + +android { + namespace = "com.dimowner.audiorecorder" + compileSdk = 34 + + defaultConfig { + applicationId = "com.dimowner.audiorecorder" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + } + + buildFeatures { + viewBinding = true + } + + signingConfigs { + create("dev") { + storeFile = file("key/debug/debug.keystore") + storePassword = "android" + keyAlias = "androiddebugkey" + keyPassword = "android" + } + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android.txt"), + "proguard-rules.pro" + ) +// firebaseCrashlytics { +// mappingFileUploadEnabled true +// } + } + getByName("debug") { + isMinifyEnabled = false + } + } + + flavorDimensions += listOf("default") + productFlavors { + create("debugConfig") { + dimension = "default" + applicationId = "com.dimowner.audiorecorder.debug" + signingConfig = signingConfigs.getByName("dev") + } + create("releaseConfig") { + dimension = "default" + signingConfig = signingConfigs.getByName("dev") + applicationId = "com.dimowner.audiorecorder" + } + } + + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + viewBinding = true + buildConfig = true + } +} + +dependencies { + ksp(libs.androidx.room.compiler) + ksp(libs.hilt.android.compiler) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.timber) + implementation(libs.androidx.viewpager2) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + implementation(libs.hilt.android) + + testImplementation(libs.junit) + testImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/browser/FileBrowserAdapter.java b/app/src/main/java/com/dimowner/audiorecorder/app/browser/FileBrowserAdapter.java index 6d9ab0a2e..1370310a4 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/browser/FileBrowserAdapter.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/browser/FileBrowserAdapter.java @@ -124,7 +124,7 @@ public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, final int position) { - final int pos = holder.getAbsoluteAdapterPosition(); + final int pos = holder.getAdapterPosition(); if (pos != RecyclerView.NO_POSITION) { RecordInfo rec = data.get(pos); holder.name.setText(rec.getName()); diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/lostrecords/LostRecordsAdapter.java b/app/src/main/java/com/dimowner/audiorecorder/app/lostrecords/LostRecordsAdapter.java index f3da3c87c..dc681ca7e 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/lostrecords/LostRecordsAdapter.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/lostrecords/LostRecordsAdapter.java @@ -86,7 +86,7 @@ public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, final int position) { - final int pos = holder.getAbsoluteAdapterPosition(); + final int pos = holder.getAdapterPosition(); if (pos != RecyclerView.NO_POSITION) { holder.name.setText(data.get(pos).getName()); holder.location.setText(data.get(pos).getPath()); diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/moverecords/MoveRecordsAdapter.kt b/app/src/main/java/com/dimowner/audiorecorder/app/moverecords/MoveRecordsAdapter.kt index b6f7c5d0e..0a64259e8 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/moverecords/MoveRecordsAdapter.kt +++ b/app/src/main/java/com/dimowner/audiorecorder/app/moverecords/MoveRecordsAdapter.kt @@ -62,7 +62,7 @@ class MoveRecordsAdapter : ListAdapter override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder is MoveRecordsItemViewHolder) { holder.bind(getItem(position)) - if (holder.absoluteAdapterPosition == activeItem) { + if (holder.adapterPosition == activeItem) { holder.binding.container.setBackgroundResource(R.color.selected_item_color) } else { holder.binding.container.setBackgroundResource(android.R.color.transparent) @@ -145,8 +145,8 @@ class MoveRecordsAdapter : ListAdapter ): RecyclerView.ViewHolder(binding.root) { init { - binding.container.setOnClickListener { itemClickListener?.invoke(bindingAdapterPosition) } - binding.btnMove.setOnClickListener { moveRecordsClickListener?.invoke(bindingAdapterPosition) } + binding.container.setOnClickListener { itemClickListener?.invoke(adapterPosition) } + binding.btnMove.setOnClickListener { moveRecordsClickListener?.invoke(adapterPosition) } binding.btnMove.background = RippleUtils.createRippleShape( ContextCompat.getColor(binding.btnMove.context, R.color.white_transparent_80), ContextCompat.getColor(binding.btnMove.context, R.color.white_transparent_50), diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java index 77e960757..be9b38d38 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/records/RecordsAdapter.java @@ -155,7 +155,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, final int pos) { if (viewHolder.getItemViewType() == ListItem.ITEM_TYPE_NORMAL) { final ItemViewHolder holder = (ItemViewHolder) viewHolder; - final int p = holder.getAbsoluteAdapterPosition(); + final int p = holder.getAdapterPosition(); final ListItem item = data.get(p); holder.name.setText(item.getName()); holder.description.setText(item.getDurationStr()); @@ -196,7 +196,7 @@ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder viewHolder, UniversalViewHolder holder = (UniversalViewHolder) viewHolder; ((TextView)holder.view).setText( TimeUtils.formatDateSmartLocale( - data.get(viewHolder.getAbsoluteAdapterPosition()).getAdded(), + data.get(viewHolder.getAdapterPosition()).getAdded(), holder.view.getContext() ) ); @@ -626,13 +626,13 @@ static class ItemViewHolder extends RecyclerView.ViewHolder { super(itemView); view = itemView; view.setOnClickListener(v -> { - int pos = getAbsoluteAdapterPosition(); + int pos = getAdapterPosition(); if (pos != RecyclerView.NO_POSITION && onItemClickListener != null) { onItemClickListener.onItemClick(pos); } }); view.setOnLongClickListener(v -> { - int pos = getAbsoluteAdapterPosition(); + int pos = getAdapterPosition(); if (pos != RecyclerView.NO_POSITION && longClickListener != null) { longClickListener.onItemLongClick(pos); } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/trash/TrashAdapter.java b/app/src/main/java/com/dimowner/audiorecorder/app/trash/TrashAdapter.java index a72c7f939..17ae58b88 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/trash/TrashAdapter.java +++ b/app/src/main/java/com/dimowner/audiorecorder/app/trash/TrashAdapter.java @@ -88,7 +88,7 @@ public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { @Override public void onBindViewHolder(@NonNull ItemViewHolder holder, final int pos) { - final int position = holder.getAbsoluteAdapterPosition(); + final int position = holder.getAdapterPosition(); if (position != RecyclerView.NO_POSITION) { holder.name.setText(data.get(position).getName()); holder.duration.setText(TimeUtils.formatTimeIntervalHourMinSec2(data.get(position).getDuration()/1000)); diff --git a/build.gradle b/build.gradle deleted file mode 100755 index 20f6cf264..000000000 --- a/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -buildscript { - ext.kotlin_version = '1.9.22' - - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - -// classpath 'com.google.gms:google-services:4.3.10' -// classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' - } -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..351ca5aff --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,15 @@ +buildscript { + repositories { + google() + mavenCentral() + } +} + +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.hilt) apply false + alias(libs.plugins.android.test) apply false +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index c097316a8..233d816cb 100755 --- a/gradle.properties +++ b/gradle.properties @@ -13,8 +13,14 @@ org.gradle.jvmargs=-Xmx1536m #android.debug.obsoleteApi=true android.useAndroidX=true android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true +#android.defaults.buildfeatures.buildconfig=true # 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 +# 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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..00fddec3c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,47 @@ +[versions] +androidGradlePlugin = "8.2.1" +androidX = "1.3.2" +coreTesting = "2.2.0" +coroutines = "1.7.3" +espressoCore = "3.5.1" +hilt = "2.50" +junit = "4.13.2" +junitVersion = "1.1.5" +kotlin = "1.9.22" +ksp = "1.9.22-1.0.16" +ktx = "1.12.0" +lifecycle = "2.7.0" +navigation = "2.7.6" +room = "2.6.1" +viewpager2 = "1.0.0" +timber = "5.0.1" + +[libraries] +androidx-arch-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "ktx" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } +androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junitVersion" } +androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycle" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidX" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } + +#gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } +hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", version.ref = "hilt" } +junit = { module = "junit:junit", version.ref = "junit" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-test = { id = "com.android.test", version.ref = "androidGradlePlugin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/settings.gradle b/settings.gradle deleted file mode 100755 index e7b4def49..000000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..5828ea0d7 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "AudioRecorder" +include(":app") + \ No newline at end of file From dd8cd4bcc67e85de25a0927f5e6d7ed7cb2649bb Mon Sep 17 00:00:00 2001 From: Dmytro Ponomarenko Date: Wed, 24 Jan 2024 23:10:55 +0200 Subject: [PATCH 03/58] Added Room database for Records with unit test coverage. --- app/build.gradle.kts | 2 +- .../audiorecorder/data/room/RecordDaoTest.kt | 266 ++++++++++++++++++ .../audiorecorder/data/room/AppDatabase.kt | 31 ++ .../audiorecorder/data/room/Converters.kt | 21 ++ .../audiorecorder/data/room/RecordDao.kt | 36 +++ .../audiorecorder/data/room/RecordEntity.kt | 69 +++++ .../audiorecorder/data/room/ConvertersTest.kt | 48 ++++ gradle/libs.versions.toml | 2 + 8 files changed, 474 insertions(+), 1 deletion(-) create mode 100644 app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/data/room/AppDatabase.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/data/room/Converters.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/data/room/RecordDao.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt create mode 100644 app/src/test/java/com/dimowner/audiorecorder/data/room/ConvertersTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f16057c24..00fd9607a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -65,7 +65,6 @@ android { } } - compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -91,6 +90,7 @@ dependencies { implementation(libs.hilt.android) testImplementation(libs.junit) + testImplementation(libs.androidx.junit.ktx) testImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt b/app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt new file mode 100644 index 000000000..1ff48a086 --- /dev/null +++ b/app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt @@ -0,0 +1,266 @@ +package com.dimowner.audiorecorder.data.room + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNull +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class RecordDaoTest { + + private lateinit var recordDao: RecordDao + private lateinit var db: AppDatabase + + @Before + fun createDb() { + val context: Context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).allowMainThreadQueries() + .build() + recordDao = db.recordDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + fun testInsertAndGetRecordById() { + val record = RecordEntity( + 1, + "Test Record", + 1000, + 123456789L, + 123456789L, + 0L, + "path/to/record", + "mp3", + 1024, + 44100, + 2, + 128, + false, + false, + IntArray(10), + ) + recordDao.insertRecord(record) + + val loaded = recordDao.getRecordById(1) + assertEquals(record, loaded) + } + + @Test + @Throws(Exception::class) + fun testUpdateRecord() = runBlocking { + val record = RecordEntity( + 1, + "Test Record", + 1000, + 123456789L, + 123456789L, + 0L, + "path/to/record", + "mp3", + 1024, + 44100, + 2, + 128, + false, + false, + IntArray(10), + ) + recordDao.insertRecord(record) + + val updatedRecord = record.copy(name = "Updated Record") + recordDao.updateRecord(updatedRecord) + + val loaded = recordDao.getRecordById(1) + assertEquals("Updated Record", loaded?.name) + } + + @Test + @Throws(Exception::class) + fun testDeleteRecord() = runBlocking { + val record = RecordEntity( + 1, + "Test Record", + 1000, + 123456789L, + 123456789L, + 0L, + "path/to/record", + "mp3", + 1024, + 44100, + 2, + 128, + false, + false, + IntArray(10), + ) + recordDao.insertRecord(record) + + recordDao.deleteRecord(record) + + val loaded = recordDao.getRecordById(1) + assertNull(loaded) + } + + @Test + fun testDeleteRecordById() { + val record = RecordEntity( + 1, + "Test Record", + 1000, + 123456789L, + 123456789L, + 0L, + "path/to/record", + "mp3", + 1024, + 44100, + 2, + 128, + false, + false, + IntArray(10), + ) + recordDao.insertRecord(record) + + recordDao.deleteRecordById(1) + val loaded = recordDao.getRecordById(1) + assertNull(loaded) + } + + @Test + fun testGetRecordsCount() { + val record1 = RecordEntity( + 1, + "Record 1", + 1000, + 123456789L, + 123456789L, + 0L, + "path/to/record1", + "mp3", + 1024, + 44100, + 2, + 128, + false, + false, + IntArray(10), + ) + val record2 = RecordEntity( + 2, + "Record 2", + 2000, + 123456790L, + 123456790L, + 0L, + "path/to/record2", + "mp3", + 2048, + 44100, + 2, + 256, + false, + false, + IntArray(10), + ) + + recordDao.insertRecord(record1) + recordDao.insertRecord(record2) + + val count = recordDao.getRecordsCount() + assertEquals(2, count) + } + + @Test + fun testDeleteAllRecords() { + val record1 = RecordEntity( + 1, + "Record 1", + 1000, + 123456789L, + 123456789L, + 0L, + "path/to/record1", + "mp3", + 1024, + 44100, + 2, + 128, + false, + false, + IntArray(10), + ) + val record2 = RecordEntity( + 2, + "Record 2", + 2000, + 123456790L, + 123456790L, + 0L, + "path/to/record2", + "mp3", + 2048, + 44100, + 2, + 256, + false, + false, + IntArray(10), + ) + + recordDao.insertRecord(record1) + recordDao.insertRecord(record2) + + recordDao.deleteAllRecords() + val count = recordDao.getRecordsCount() + assertEquals(0, count) + } + + @Test + fun testGetRecordsByPage() { + val records = Array(100) { + RecordEntity( + it + 1, + "Record $it", + 1000L + it, + 123456789L + it, + 123456789L + it, + 0L, + "path/to/record$it", + "mp3", + 1024, + 44100, + 2, + 128, + false, + false, + IntArray(10), + ) + } + + records.forEach { + recordDao.insertRecord(it) + } + + val pageSize = 20 + val offset = 40 + val recordsByPage = recordDao.getRecordsByPage(pageSize, offset) + assertEquals(pageSize, recordsByPage.size) + val expected = records.slice(offset until (offset + pageSize)) + assertEquals(expected, recordsByPage) + } +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/room/AppDatabase.kt b/app/src/main/java/com/dimowner/audiorecorder/data/room/AppDatabase.kt new file mode 100644 index 000000000..8318d5995 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/data/room/AppDatabase.kt @@ -0,0 +1,31 @@ +package com.dimowner.audiorecorder.data.room + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +const val DATABASE_NAME = "app_database" + +@Database(entities = [RecordEntity::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + + abstract fun recordDao(): RecordDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + fun getDatabase(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + DATABASE_NAME + ).build() + INSTANCE = instance + instance + } + } + } +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/room/Converters.kt b/app/src/main/java/com/dimowner/audiorecorder/data/room/Converters.kt new file mode 100644 index 000000000..b6cd04693 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/data/room/Converters.kt @@ -0,0 +1,21 @@ +package com.dimowner.audiorecorder.data.room + +import androidx.room.TypeConverter + +class Converters { + + @TypeConverter + fun fromIntArray(intArray: IntArray): String { + return intArray.joinToString(separator = ",") + } + + @TypeConverter + fun toIntArray(value: String): IntArray { + return if (value.isBlank()) { + // If the input string is blank, return an empty IntArray. + intArrayOf() + } else { + value.split(",").map { it.toInt() }.toIntArray() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordDao.kt b/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordDao.kt new file mode 100644 index 000000000..b58e9126e --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordDao.kt @@ -0,0 +1,36 @@ +package com.dimowner.audiorecorder.data.room + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import androidx.room.Delete + +@Dao +interface RecordDao { + + @Query("SELECT * FROM records WHERE id = :recordId") + fun getRecordById(recordId: Int): RecordEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertRecord(record: RecordEntity) + + @Update + fun updateRecord(record: RecordEntity) + + @Delete + fun deleteRecord(record: RecordEntity) + + @Query("DELETE FROM records WHERE id = :recordId") + fun deleteRecordById(recordId: Int) + + @Query("DELETE FROM records") + fun deleteAllRecords() + + @Query("SELECT COUNT(*) FROM records") + fun getRecordsCount(): Int + + @Query("SELECT * FROM records ORDER BY id LIMIT :pageSize OFFSET :offset") + fun getRecordsByPage(pageSize: Int, offset: Int): List +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt b/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt new file mode 100644 index 000000000..824224d92 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt @@ -0,0 +1,69 @@ +package com.dimowner.audiorecorder.data.room + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters + +@Entity(tableName = "records") +@TypeConverters(Converters::class) +data class RecordEntity( + @PrimaryKey val id: Int, + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "duration") val duration: Long, + @ColumnInfo(name = "created") val created: Long, + @ColumnInfo(name = "added") val added: Long, + @ColumnInfo(name = "removed") val removed: Long, + @ColumnInfo(name = "path") var path: String, + @ColumnInfo(name = "format") val format: String, + @ColumnInfo(name = "size") val size: Long, + @ColumnInfo(name = "sampleRate") val sampleRate: Int, + @ColumnInfo(name = "channelCount") val channelCount: Int, + @ColumnInfo(name = "bitrate") val bitrate: Int, + @ColumnInfo(name = "isBookmarked") val isBookmarked: Boolean, + @ColumnInfo(name = "isWaveformProcessed") val isWaveformProcessed: Boolean, + @ColumnInfo(name = "amps") val amps: IntArray, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as RecordEntity + + if (id != other.id) return false + if (name != other.name) return false + if (duration != other.duration) return false + if (created != other.created) return false + if (added != other.added) return false + if (removed != other.removed) return false + if (path != other.path) return false + if (format != other.format) return false + if (size != other.size) return false + if (sampleRate != other.sampleRate) return false + if (channelCount != other.channelCount) return false + if (bitrate != other.bitrate) return false + if (isBookmarked != other.isBookmarked) return false + if (isWaveformProcessed != other.isWaveformProcessed) return false + return amps.contentEquals(other.amps) + } + + override fun hashCode(): Int { + var result = id + result = 31 * result + name.hashCode() + result = 31 * result + duration.hashCode() + result = 31 * result + created.hashCode() + result = 31 * result + added.hashCode() + result = 31 * result + removed.hashCode() + result = 31 * result + path.hashCode() + result = 31 * result + format.hashCode() + result = 31 * result + size.hashCode() + result = 31 * result + sampleRate + result = 31 * result + channelCount + result = 31 * result + bitrate + result = 31 * result + isBookmarked.hashCode() + result = 31 * result + isWaveformProcessed.hashCode() + result = 31 * result + amps.contentHashCode() + return result + } +} diff --git a/app/src/test/java/com/dimowner/audiorecorder/data/room/ConvertersTest.kt b/app/src/test/java/com/dimowner/audiorecorder/data/room/ConvertersTest.kt new file mode 100644 index 000000000..78ce3e872 --- /dev/null +++ b/app/src/test/java/com/dimowner/audiorecorder/data/room/ConvertersTest.kt @@ -0,0 +1,48 @@ +package com.dimowner.audiorecorder.data.room + +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Test + +class ConvertersTest { + + @Test + fun test_fromIntArray() { + val intArray = intArrayOf(1, 2, 3, 4, 5) + val expectedString = "1,2,3,4,5" + + val result = Converters().fromIntArray(intArray) + + assertEquals(expectedString, result) + } + + @Test + fun test_fromIntArray_withEmptyArray() { + val intArray = intArrayOf() + val expectedString = "" + + val result = Converters().fromIntArray(intArray) + + assertEquals(expectedString, result) + } + + @Test + fun test_toIntArray() { + val stringValue = "1,2,3,4,5" + val expectedIntArray = intArrayOf(1, 2, 3, 4, 5) + + val result = Converters().toIntArray(stringValue) + + assertArrayEquals(expectedIntArray, result) + } + + @Test + fun test_toIntArray_withEmptyString() { + val stringValue = "" + val expectedIntArray = intArrayOf() + + val result = Converters().toIntArray(stringValue) + + assertArrayEquals(expectedIntArray, result) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 00fddec3c..a882e048b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,6 +15,7 @@ navigation = "2.7.6" room = "2.6.1" viewpager2 = "1.0.0" timber = "5.0.1" +junitKtx = "1.1.5" [libraries] androidx-arch-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" } @@ -38,6 +39,7 @@ kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutine kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } +androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From 2fc872960aa32cc37c3ed829d4372ffb17fa5730 Mon Sep 17 00:00:00 2001 From: Dmytro Ponomarenko Date: Thu, 25 Jan 2024 21:44:09 +0200 Subject: [PATCH 04/58] Added ability to mark records as moved to recycle bin. --- .../dimowner/audiorecorder/data/room/RecordDaoTest.kt | 9 +++++++++ .../com/dimowner/audiorecorder/data/room/RecordEntity.kt | 3 +++ 2 files changed, 12 insertions(+) diff --git a/app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt b/app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt index 1ff48a086..e5006911e 100644 --- a/app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt +++ b/app/src/androidTest/java/com/dimowner/audiorecorder/data/room/RecordDaoTest.kt @@ -50,6 +50,7 @@ class RecordDaoTest { 128, false, false, + false, IntArray(10), ) recordDao.insertRecord(record) @@ -76,6 +77,7 @@ class RecordDaoTest { 128, false, false, + false, IntArray(10), ) recordDao.insertRecord(record) @@ -105,6 +107,7 @@ class RecordDaoTest { 128, false, false, + false, IntArray(10), ) recordDao.insertRecord(record) @@ -132,6 +135,7 @@ class RecordDaoTest { 128, false, false, + false, IntArray(10), ) recordDao.insertRecord(record) @@ -158,6 +162,7 @@ class RecordDaoTest { 128, false, false, + false, IntArray(10), ) val record2 = RecordEntity( @@ -175,6 +180,7 @@ class RecordDaoTest { 256, false, false, + false, IntArray(10), ) @@ -202,6 +208,7 @@ class RecordDaoTest { 128, false, false, + false, IntArray(10), ) val record2 = RecordEntity( @@ -219,6 +226,7 @@ class RecordDaoTest { 256, false, false, + false, IntArray(10), ) @@ -248,6 +256,7 @@ class RecordDaoTest { 128, false, false, + false, IntArray(10), ) } diff --git a/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt b/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt index 824224d92..7838fecd2 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt +++ b/app/src/main/java/com/dimowner/audiorecorder/data/room/RecordEntity.kt @@ -22,6 +22,7 @@ data class RecordEntity( @ColumnInfo(name = "bitrate") val bitrate: Int, @ColumnInfo(name = "isBookmarked") val isBookmarked: Boolean, @ColumnInfo(name = "isWaveformProcessed") val isWaveformProcessed: Boolean, + @ColumnInfo(name = "isMovedToRecycle") val isMovedToRecycle: Boolean, @ColumnInfo(name = "amps") val amps: IntArray, ) { @@ -45,6 +46,7 @@ data class RecordEntity( if (bitrate != other.bitrate) return false if (isBookmarked != other.isBookmarked) return false if (isWaveformProcessed != other.isWaveformProcessed) return false + if (isMovedToRecycle != other.isMovedToRecycle) return false return amps.contentEquals(other.amps) } @@ -63,6 +65,7 @@ data class RecordEntity( result = 31 * result + bitrate result = 31 * result + isBookmarked.hashCode() result = 31 * result + isWaveformProcessed.hashCode() + result = 31 * result + isMovedToRecycle.hashCode() result = 31 * result + amps.contentHashCode() return result } From dccfed395fd0c08e684d4c6f43002a6bd07d7caa Mon Sep 17 00:00:00 2001 From: Dmytro Ponomarenko Date: Sun, 28 Jan 2024 12:17:25 +0200 Subject: [PATCH 05/58] Added Compose to the app and some test screens made with compose. --- app/build.gradle.kts | 26 ++- app/src/main/AndroidManifest.xml | 4 +- .../dimowner/audiorecorder/ARApplication.kt | 6 +- .../audiorecorder/app/v2/ui/AppComponents.kt | 204 ++++++++++++++++++ .../app/v2/ui/FunFactsNavigationGraph.kt | 31 +++ .../audiorecorder/app/v2/ui/HomeActivity.kt | 29 +++ .../audiorecorder/app/v2/ui/Routes.kt | 9 + .../app/v2/ui/UserInputScreen.kt | 84 ++++++++ .../app/v2/ui/UserInputViewModel.kt | 39 ++++ .../audiorecorder/app/v2/ui/WelcomeScreen.kt | 44 ++++ .../audiorecorder/app/v2/ui/theme/Color.kt | 11 + .../audiorecorder/app/v2/ui/theme/Theme.kt | 70 ++++++ .../audiorecorder/app/v2/ui/theme/Type.kt | 34 +++ gradle/libs.versions.toml | 36 ++++ 14 files changed, 622 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/AppComponents.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/HomeActivity.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputViewModel.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/WelcomeScreen.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 00fd9607a..8554ecc8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,7 +64,6 @@ android { applicationId = "com.dimowner.audiorecorder" } } - compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 @@ -75,6 +74,10 @@ android { buildFeatures { viewBinding = true buildConfig = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() } } @@ -88,10 +91,29 @@ dependencies { implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) implementation(libs.hilt.android) + implementation(libs.exoplayer.core) + implementation(libs.exoplayer.ui) + implementation(libs.androidx.core.splashscreen) + + // Compose + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.constraintlayout.compose) + implementation(libs.androidx.compose.runtime) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui.viewbinding) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.runtime.livedata) + implementation(libs.androidx.lifecycle.viewmodel.compose) + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.navigation.compose) testImplementation(libs.junit) testImplementation(libs.androidx.junit.ktx) testImplementation(libs.kotlinx.coroutines.test) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) -} \ No newline at end of file +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 93ea6c382..466dae346 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -53,6 +53,8 @@ + + @@ -105,4 +107,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/dimowner/audiorecorder/ARApplication.kt b/app/src/main/java/com/dimowner/audiorecorder/ARApplication.kt index e942b1ea9..da9bf71f9 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/ARApplication.kt +++ b/app/src/main/java/com/dimowner/audiorecorder/ARApplication.kt @@ -32,11 +32,13 @@ import android.telephony.TelephonyManager import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import com.dimowner.audiorecorder.util.AndroidUtils +import dagger.hilt.android.HiltAndroidApp import timber.log.Timber import timber.log.Timber.DebugTree - //import com.google.firebase.FirebaseApp; + +@HiltAndroidApp class ARApplication : Application() { private var audioOutputChangeReceiver: AudioOutputChangeReceiver? = null override fun onCreate() { @@ -173,4 +175,4 @@ class ARApplication : Application() { val longWaveformSampleCount: Int get() = (AppConstants.WAVEFORM_WIDTH * screenWidthDp).toInt() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/AppComponents.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/AppComponents.kt new file mode 100644 index 000000000..c9a91dac6 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/AppComponents.kt @@ -0,0 +1,204 @@ +package com.dimowner.audiorecorder.app.v2.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.dimowner.audiorecorder.R + +@Composable +fun TopBar(value: String) { + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text( + text = value, + color = Color.Black, + fontSize = 24.sp, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.weight(1f)) + Image( + modifier = Modifier.size(64.dp), + painter = painterResource(id = R.drawable.ic_bookmark), + contentDescription = "My test image" + ) + } +} + +@Preview(showBackground = true) +@Composable +fun TopBarPreview() { + TopBar("Text") +} + +@Composable +fun TextComponent( + textValue: String, + textSize: TextUnit, + colorValue: Color = Color.Black, + fontWeight: FontWeight = FontWeight.Light +) { + Text( + text = textValue, + fontSize = textSize, + color = colorValue, + fontWeight = fontWeight, + ) +} + +@Preview(showBackground = true) +@Composable +fun TextComponentPreview() { + TextComponent(textValue = "Text to preview", textSize = 24.sp) +} + +@Composable +fun TextFieldComponent(onTextChanged: (name: String) -> Unit) { + + var currentValue by remember { + mutableStateOf("") + } + + val localFocusManager = LocalFocusManager.current + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = currentValue, + onValueChange = { + currentValue = it + onTextChanged(it) + }, + placeholder = { + Text(text = "Enter your name", fontSize = 18.sp) + }, + textStyle = TextStyle.Default.copy(fontSize = 24.sp), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions { + localFocusManager.clearFocus() + } + ) +} + +@Preview(showBackground = true) +@Composable +fun TextFieldComponentPreview() { + TextFieldComponent {} +} + +@Composable +fun AnimalCard(image: Int, selected: Boolean, onImageClicked: (animalName: String) -> Unit) { + val localFocusManager = LocalFocusManager.current + Card( + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .padding(16.dp) + .size(130.dp) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .border( + width = 1.dp, + color = if (selected) Color.Green else Color.Transparent, + shape = RoundedCornerShape(8.dp), + ), + ) { + Image( + modifier = Modifier + .padding(16.dp) + .wrapContentWidth() + .wrapContentHeight() + .clickable { + val animalName = if (image == R.drawable.ic_audiotrack_64) "Cat" else "Dog" + onImageClicked(animalName) + localFocusManager.clearFocus() + }, + painter = painterResource(id = image), + contentDescription = "Animal image", + ) + } + } +} + +@Preview() +@Composable +fun AnimalCardPreview() { + AnimalCard(image = R.drawable.ic_color_lens, false) {} +} + + +@Composable +fun ButtonComponent(onClicked: () -> Unit) { + Button( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth(), + onClick = { onClicked() } + ) { + TextComponent(textValue = "Go to details screen", textSize = 18.sp, colorValue = Color.White) + } +} + +@Preview() +@Composable +fun ButtonComponentPreview() { + ButtonComponent {} +} + +@Composable +fun InfoCard(animalSelected: String?) { + Card( + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .padding(16.dp), + elevation = CardDefaults.cardElevation() + ) { + Text( + modifier = Modifier.padding(18.dp, 24.dp), + text = if (animalSelected == "Dog") "Dog info" else if (animalSelected == "Cat") "Cat info" else "", + fontSize = 18.sp, + color = Color.Black, + fontWeight = FontWeight.Medium, + + ) + } +} + +@Preview() +@Composable +fun InfoCardPreview() { + InfoCard("This is information card to provide some info") +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt new file mode 100644 index 000000000..8b85c7791 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt @@ -0,0 +1,31 @@ +package com.dimowner.audiorecorder.app.v2.ui + +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument + +@Composable +fun FunFactsNavigationGraph(userInputViewModel: UserInputViewModel = viewModel()) { + val navController = rememberNavController() + NavHost(navController = navController, startDestination = Routes.USER_INPUT_SCREEN) { + composable(Routes.USER_INPUT_SCREEN) { + UserInputScreen(navController, userInputViewModel, showWelcomeScreen = { + navController.navigate(Routes.WELCOME_SCREEN+"/${it.first}/${it.second}") + }) + } + composable("${Routes.WELCOME_SCREEN}/{${Routes.USER_NAME}}/{${Routes.ANIMAL_SELECTED}}", + arguments = listOf( + navArgument(name = Routes.USER_NAME) { type = NavType.StringType }, + navArgument(name = Routes.ANIMAL_SELECTED) { type = NavType.StringType } + ) + ) { + val userName = it.arguments?.getString(Routes.USER_NAME) + val animalSelected = it.arguments?.getString(Routes.ANIMAL_SELECTED) + WelcomeScreen(userName = userName, animalSelected = animalSelected) + } + } +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/HomeActivity.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/HomeActivity.kt new file mode 100644 index 000000000..cf672bc04 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/HomeActivity.kt @@ -0,0 +1,29 @@ +package com.dimowner.audiorecorder.app.v2.ui + +import androidx.activity.ComponentActivity +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.runtime.Composable +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import com.dimowner.audiorecorder.app.v2.ui.theme.Compose1Theme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HomeActivity: ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + installSplashScreen() + setContent { + Compose1Theme { + // A surface container using the 'background' color from the theme + FunFactsApp() + } + } + } + + @Composable + fun FunFactsApp() { + FunFactsNavigationGraph() + } +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt new file mode 100644 index 000000000..a770b23f3 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt @@ -0,0 +1,9 @@ +package com.dimowner.audiorecorder.app.v2.ui + +object Routes { + const val USER_INPUT_SCREEN = "USER_INPUT_SCREEN" + const val WELCOME_SCREEN = "WELCOME_SCREEN" + + const val USER_NAME = "NAME" + const val ANIMAL_SELECTED = "ANIMAL_SELECTED" +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt new file mode 100644 index 000000000..75a217121 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt @@ -0,0 +1,84 @@ +package com.dimowner.audiorecorder.app.v2.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.dimowner.audiorecorder.R + +@Composable +fun UserInputScreen( + navController: NavHostController, + userInputViewModel: UserInputViewModel, + showWelcomeScreen: (Pair) -> Unit +) { + + Surface( + modifier = Modifier + .fillMaxSize() +// .clickable { +// navController.navigate(Routes.WELCOME_SCREEN) +// }, + ) { + Column(modifier = Modifier + .fillMaxSize() + .padding(16.dp)) { + TopBar("Hi there \uD83D\uDE0A") + TextComponent(textValue = "Lets learn about you", textSize = 24.sp) + Spacer(modifier = Modifier.size(20.dp)) + TextComponent( + textValue = "This app will prepare a details page based on input provided by you!", + textSize = 18.sp + ) + Spacer(modifier = Modifier.size(60.dp)) + TextComponent(textValue = "Name", textSize = 18.sp) + Spacer(modifier = Modifier.size(10.dp)) + TextFieldComponent(onTextChanged = { + userInputViewModel.onEvent(UserDataUiEvents.UserNameEntered(it)) + }) + Spacer(modifier = Modifier.size(20.dp)) + TextComponent(textValue = "What do you like?", textSize = 18.sp) + + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { + AnimalCard(image = R.drawable.ic_audiotrack_64, onImageClicked = { + userInputViewModel.onEvent(UserDataUiEvents.AnimalSelected(it)) + }, selected = userInputViewModel.uiState.value.animalSelected == "Cat") + AnimalCard(image = R.drawable.ic_color_lens, onImageClicked = { + userInputViewModel.onEvent(UserDataUiEvents.AnimalSelected(it)) + }, selected = userInputViewModel.uiState.value.animalSelected == "Dog") + } + Spacer(modifier = Modifier.weight(1f)) + if (userInputViewModel.isValidState()) { + ButtonComponent(onClicked = { +// navController.navigate(Routes.WELCOME_SCREEN) + showWelcomeScreen( + Pair( + userInputViewModel.uiState.value.nameEntered, + userInputViewModel.uiState.value.animalSelected + ) + ) + }) + } + } + } +} + +@Preview +@Composable +fun UserInputScreenPreview() { + UserInputScreen(rememberNavController(), viewModel(), {}) +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputViewModel.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputViewModel.kt new file mode 100644 index 000000000..2d734b973 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputViewModel.kt @@ -0,0 +1,39 @@ +package com.dimowner.audiorecorder.app.v2.ui + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel + +class UserInputViewModel: ViewModel() { + + val uiState = mutableStateOf(UserInputScreenState()) + + fun onEvent(event: UserDataUiEvents) { + when(event) { + is UserDataUiEvents.UserNameEntered -> { + uiState.value = uiState.value.copy( + nameEntered = event.name + ) + } + is UserDataUiEvents.AnimalSelected -> { + uiState.value = uiState.value.copy( + animalSelected = event.animalValue + ) + } + } + + } + + fun isValidState(): Boolean { + return uiState.value.animalSelected.isNotEmpty() && uiState.value.nameEntered.isNotEmpty() + } +} + +data class UserInputScreenState( + val nameEntered: String = "", + val animalSelected: String = "" +) + +sealed class UserDataUiEvents { + data class UserNameEntered(val name: String): UserDataUiEvents() + data class AnimalSelected(val animalValue: String): UserDataUiEvents() +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/WelcomeScreen.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/WelcomeScreen.kt new file mode 100644 index 000000000..fc76c6f2f --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/WelcomeScreen.kt @@ -0,0 +1,44 @@ +package com.dimowner.audiorecorder.app.v2.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun WelcomeScreen(userName: String?, animalSelected: String?) { + Surface( + modifier = Modifier.fillMaxSize() + ) { + Column(modifier = Modifier + .fillMaxSize() + .padding(16.dp)) { + TopBar("Welcome $userName \uD83D\uDE0A") + TextComponent(textValue = "Thank you for sharing your data!", textSize = 24.sp) + Spacer(modifier = Modifier.size(60.dp)) + TextComponent( + textValue = "You are $animalSelected lover!", + textSize = 24.sp, + fontWeight = FontWeight.Normal + ) + Spacer(modifier = Modifier.size(16.dp)) + InfoCard(animalSelected = animalSelected) + + } + } +} + + +@Preview +@Composable +fun WelcomeScreenPreview() { + WelcomeScreen("name", "animal") +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt new file mode 100644 index 000000000..7044fe72f --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.dimowner.audiorecorder.app.v2.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt new file mode 100644 index 000000000..4fcda3581 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt @@ -0,0 +1,70 @@ +package com.dimowner.audiorecorder.app.v2.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun Compose1Theme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colorScheme.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt new file mode 100644 index 000000000..4dc2ad24b --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package com.dimowner.audiorecorder.app.v2.ui.theme + +import androidx.compose.material3.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( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a882e048b..e29abe5fe 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,15 @@ [versions] androidGradlePlugin = "8.2.1" +# @keep +compose-compiler = "1.5.8" +composeBom = "2023.10.01" +composeLatest = "1.6.0-rc01" +constraintLayoutCompose = "1.0.1" +activityCompose = "1.8.2" +coreSplashscreen = "1.0.1" +hiltNavigationCompose = "1.1.0" +viewModelCompose = "2.7.0" +material3 = "1.2.0-beta02" androidX = "1.3.2" coreTesting = "2.2.0" coroutines = "1.7.3" @@ -16,8 +26,28 @@ room = "2.6.1" viewpager2 = "1.0.0" timber = "5.0.1" junitKtx = "1.1.5" +exoPlayer = "2.19.1" +appcompat = "1.6.1" +fragment = "1.6.1" [libraries] +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "composeLatest" } +androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout", version.ref = "composeLatest" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } +androidx-compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "composeLatest" } +androidx-compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata", version.ref = "composeLatest" } +androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "composeLatest" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "composeLatest" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-ui-viewbinding = { module = "androidx.compose.ui:ui-viewbinding" } +androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintLayoutCompose" } +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "viewModelCompose" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" } +hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } androidx-arch-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "coreTesting" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "ktx" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } @@ -30,6 +60,10 @@ androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } +exoplayer-core = { module = "com.google.android.exoplayer:exoplayer-core", version.ref = "exoPlayer" } +exoplayer-dash = { module = "com.google.android.exoplayer:exoplayer-dash", version.ref = "exoPlayer" } +exoplayer-ui = { module = "com.google.android.exoplayer:exoplayer-ui", version.ref = "exoPlayer" } + #gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } @@ -40,6 +74,8 @@ kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-c kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" } androidx-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "junitKtx" } +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +androidx-fragment = { group = "androidx.fragment:fragment-ktx:", version.ref = "fragment" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From 632a05084b65ed852e08373a3a16da94385a8254 Mon Sep 17 00:00:00 2001 From: Dmytro Ponomarenko Date: Sun, 18 Feb 2024 00:59:47 +0200 Subject: [PATCH 06/58] Compose Record info screen and Settings scree UI only. --- app/build.gradle.kts | 1 + .../app/v2/ui/FunFactsNavigationGraph.kt | 31 -- .../audiorecorder/app/v2/ui/Routes.kt | 9 - .../app/v2/ui/UserInputScreen.kt | 84 --- .../audiorecorder/app/v2/ui/theme/Color.kt | 11 - .../audiorecorder/app/v2/ui/theme/Theme.kt | 70 --- .../audiorecorder/app/v2/ui/theme/Type.kt | 34 -- .../{app/v2/ui => v2}/AppComponents.kt | 482 ++++++++++-------- .../v2/ComposePlaygroundScreen.kt | 219 ++++++++ .../{app/v2/ui => v2}/HomeActivity.kt | 17 +- .../{app/v2/ui => v2}/UserInputViewModel.kt | 78 +-- .../{app/v2/ui => v2}/WelcomeScreen.kt | 87 ++-- .../audiorecorder/v2/info/AssetParamType.kt | 25 + .../audiorecorder/v2/info/RecordInfoScreen.kt | 61 +++ .../audiorecorder/v2/info/RecordInfoState.kt | 22 + .../v2/navigation/RecorerNavigationGraph.kt | 113 ++++ .../audiorecorder/v2/navigation/Routes.kt | 26 + .../v2/settings/SettingsComponents.kt | 450 ++++++++++++++++ .../v2/settings/SettingsScreen.kt | 122 +++++ .../v2/settings/SettingsState.kt | 41 ++ .../dimowner/audiorecorder/v2/theme/Color.kt | 92 ++++ .../dimowner/audiorecorder/v2/theme/Shapes.kt | 13 + .../dimowner/audiorecorder/v2/theme/Theme.kt | 188 +++++++ .../dimowner/audiorecorder/v2/theme/Type.kt | 41 ++ app/src/main/res/values/styles.xml | 2 + gradle/libs.versions.toml | 6 +- 26 files changed, 1790 insertions(+), 535 deletions(-) delete mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt delete mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt delete mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt delete mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt delete mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt delete mode 100644 app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt rename app/src/main/java/com/dimowner/audiorecorder/{app/v2/ui => v2}/AppComponents.kt (60%) create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/ComposePlaygroundScreen.kt rename app/src/main/java/com/dimowner/audiorecorder/{app/v2/ui => v2}/HomeActivity.kt (53%) rename app/src/main/java/com/dimowner/audiorecorder/{app/v2/ui => v2}/UserInputViewModel.kt (92%) rename app/src/main/java/com/dimowner/audiorecorder/{app/v2/ui => v2}/WelcomeScreen.kt (90%) create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/info/AssetParamType.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoScreen.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoState.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/navigation/RecorerNavigationGraph.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/navigation/Routes.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsComponents.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsScreen.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsState.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/theme/Color.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/theme/Shapes.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/theme/Theme.kt create mode 100644 app/src/main/java/com/dimowner/audiorecorder/v2/theme/Type.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8554ecc8b..2006e4096 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -94,6 +94,7 @@ dependencies { implementation(libs.exoplayer.core) implementation(libs.exoplayer.ui) implementation(libs.androidx.core.splashscreen) + implementation(libs.gson) // Compose implementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt deleted file mode 100644 index 8b85c7791..000000000 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/FunFactsNavigationGraph.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.dimowner.audiorecorder.app.v2.ui - -import androidx.compose.runtime.Composable -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavType -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument - -@Composable -fun FunFactsNavigationGraph(userInputViewModel: UserInputViewModel = viewModel()) { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = Routes.USER_INPUT_SCREEN) { - composable(Routes.USER_INPUT_SCREEN) { - UserInputScreen(navController, userInputViewModel, showWelcomeScreen = { - navController.navigate(Routes.WELCOME_SCREEN+"/${it.first}/${it.second}") - }) - } - composable("${Routes.WELCOME_SCREEN}/{${Routes.USER_NAME}}/{${Routes.ANIMAL_SELECTED}}", - arguments = listOf( - navArgument(name = Routes.USER_NAME) { type = NavType.StringType }, - navArgument(name = Routes.ANIMAL_SELECTED) { type = NavType.StringType } - ) - ) { - val userName = it.arguments?.getString(Routes.USER_NAME) - val animalSelected = it.arguments?.getString(Routes.ANIMAL_SELECTED) - WelcomeScreen(userName = userName, animalSelected = animalSelected) - } - } -} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt deleted file mode 100644 index a770b23f3..000000000 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/Routes.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.dimowner.audiorecorder.app.v2.ui - -object Routes { - const val USER_INPUT_SCREEN = "USER_INPUT_SCREEN" - const val WELCOME_SCREEN = "WELCOME_SCREEN" - - const val USER_NAME = "NAME" - const val ANIMAL_SELECTED = "ANIMAL_SELECTED" -} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt deleted file mode 100644 index 75a217121..000000000 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputScreen.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.dimowner.audiorecorder.app.v2.ui - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.navigation.NavHostController -import androidx.navigation.compose.rememberNavController -import com.dimowner.audiorecorder.R - -@Composable -fun UserInputScreen( - navController: NavHostController, - userInputViewModel: UserInputViewModel, - showWelcomeScreen: (Pair) -> Unit -) { - - Surface( - modifier = Modifier - .fillMaxSize() -// .clickable { -// navController.navigate(Routes.WELCOME_SCREEN) -// }, - ) { - Column(modifier = Modifier - .fillMaxSize() - .padding(16.dp)) { - TopBar("Hi there \uD83D\uDE0A") - TextComponent(textValue = "Lets learn about you", textSize = 24.sp) - Spacer(modifier = Modifier.size(20.dp)) - TextComponent( - textValue = "This app will prepare a details page based on input provided by you!", - textSize = 18.sp - ) - Spacer(modifier = Modifier.size(60.dp)) - TextComponent(textValue = "Name", textSize = 18.sp) - Spacer(modifier = Modifier.size(10.dp)) - TextFieldComponent(onTextChanged = { - userInputViewModel.onEvent(UserDataUiEvents.UserNameEntered(it)) - }) - Spacer(modifier = Modifier.size(20.dp)) - TextComponent(textValue = "What do you like?", textSize = 18.sp) - - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) { - AnimalCard(image = R.drawable.ic_audiotrack_64, onImageClicked = { - userInputViewModel.onEvent(UserDataUiEvents.AnimalSelected(it)) - }, selected = userInputViewModel.uiState.value.animalSelected == "Cat") - AnimalCard(image = R.drawable.ic_color_lens, onImageClicked = { - userInputViewModel.onEvent(UserDataUiEvents.AnimalSelected(it)) - }, selected = userInputViewModel.uiState.value.animalSelected == "Dog") - } - Spacer(modifier = Modifier.weight(1f)) - if (userInputViewModel.isValidState()) { - ButtonComponent(onClicked = { -// navController.navigate(Routes.WELCOME_SCREEN) - showWelcomeScreen( - Pair( - userInputViewModel.uiState.value.nameEntered, - userInputViewModel.uiState.value.animalSelected - ) - ) - }) - } - } - } -} - -@Preview -@Composable -fun UserInputScreenPreview() { - UserInputScreen(rememberNavController(), viewModel(), {}) -} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt deleted file mode 100644 index 7044fe72f..000000000 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.dimowner.audiorecorder.app.v2.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt deleted file mode 100644 index 4fcda3581..000000000 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Theme.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.dimowner.audiorecorder.app.v2.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.WindowCompat - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun Compose1Theme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - val window = (view.context as Activity).window - window.statusBarColor = colorScheme.primary.toArgb() - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt b/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt deleted file mode 100644 index 4dc2ad24b..000000000 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.dimowner.audiorecorder.app.v2.ui.theme - -import androidx.compose.material3.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( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/AppComponents.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/AppComponents.kt similarity index 60% rename from app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/AppComponents.kt rename to app/src/main/java/com/dimowner/audiorecorder/v2/AppComponents.kt index c9a91dac6..faac19a05 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/AppComponents.kt +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/AppComponents.kt @@ -1,204 +1,278 @@ -package com.dimowner.audiorecorder.app.v2.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.dimowner.audiorecorder.R - -@Composable -fun TopBar(value: String) { - Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - Text( - text = value, - color = Color.Black, - fontSize = 24.sp, - fontWeight = FontWeight.Medium - ) - Spacer(modifier = Modifier.weight(1f)) - Image( - modifier = Modifier.size(64.dp), - painter = painterResource(id = R.drawable.ic_bookmark), - contentDescription = "My test image" - ) - } -} - -@Preview(showBackground = true) -@Composable -fun TopBarPreview() { - TopBar("Text") -} - -@Composable -fun TextComponent( - textValue: String, - textSize: TextUnit, - colorValue: Color = Color.Black, - fontWeight: FontWeight = FontWeight.Light -) { - Text( - text = textValue, - fontSize = textSize, - color = colorValue, - fontWeight = fontWeight, - ) -} - -@Preview(showBackground = true) -@Composable -fun TextComponentPreview() { - TextComponent(textValue = "Text to preview", textSize = 24.sp) -} - -@Composable -fun TextFieldComponent(onTextChanged: (name: String) -> Unit) { - - var currentValue by remember { - mutableStateOf("") - } - - val localFocusManager = LocalFocusManager.current - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - value = currentValue, - onValueChange = { - currentValue = it - onTextChanged(it) - }, - placeholder = { - Text(text = "Enter your name", fontSize = 18.sp) - }, - textStyle = TextStyle.Default.copy(fontSize = 24.sp), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions { - localFocusManager.clearFocus() - } - ) -} - -@Preview(showBackground = true) -@Composable -fun TextFieldComponentPreview() { - TextFieldComponent {} -} - -@Composable -fun AnimalCard(image: Int, selected: Boolean, onImageClicked: (animalName: String) -> Unit) { - val localFocusManager = LocalFocusManager.current - Card( - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .padding(16.dp) - .size(130.dp) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .border( - width = 1.dp, - color = if (selected) Color.Green else Color.Transparent, - shape = RoundedCornerShape(8.dp), - ), - ) { - Image( - modifier = Modifier - .padding(16.dp) - .wrapContentWidth() - .wrapContentHeight() - .clickable { - val animalName = if (image == R.drawable.ic_audiotrack_64) "Cat" else "Dog" - onImageClicked(animalName) - localFocusManager.clearFocus() - }, - painter = painterResource(id = image), - contentDescription = "Animal image", - ) - } - } -} - -@Preview() -@Composable -fun AnimalCardPreview() { - AnimalCard(image = R.drawable.ic_color_lens, false) {} -} - - -@Composable -fun ButtonComponent(onClicked: () -> Unit) { - Button( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - onClick = { onClicked() } - ) { - TextComponent(textValue = "Go to details screen", textSize = 18.sp, colorValue = Color.White) - } -} - -@Preview() -@Composable -fun ButtonComponentPreview() { - ButtonComponent {} -} - -@Composable -fun InfoCard(animalSelected: String?) { - Card( - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .padding(16.dp), - elevation = CardDefaults.cardElevation() - ) { - Text( - modifier = Modifier.padding(18.dp, 24.dp), - text = if (animalSelected == "Dog") "Dog info" else if (animalSelected == "Cat") "Cat info" else "", - fontSize = 18.sp, - color = Color.Black, - fontWeight = FontWeight.Medium, - - ) - } -} - -@Preview() -@Composable -fun InfoCardPreview() { - InfoCard("This is information card to provide some info") -} +package com.dimowner.audiorecorder.v2 + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.DeviceFontFamilyName +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.dimowner.audiorecorder.R + +@Composable +fun TextComponent( + textValue: String, + textSize: TextUnit, + fontWeight: FontWeight = FontWeight.Light +) { + Text( + text = textValue, + fontSize = textSize, + fontWeight = fontWeight, + ) +} + +@Preview(showBackground = true) +@Composable +fun TextComponentPreview() { + TextComponent(textValue = "Text to preview", textSize = 24.sp) +} + +@Composable +fun TextFieldComponent(onTextChanged: (name: String) -> Unit) { + + var currentValue by remember { + mutableStateOf("") + } + + val localFocusManager = LocalFocusManager.current + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = currentValue, + onValueChange = { + currentValue = it + onTextChanged(it) + }, + placeholder = { + Text(text = "Enter your name", fontSize = 18.sp) + }, + textStyle = TextStyle.Default.copy(fontSize = 24.sp), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions { + localFocusManager.clearFocus() + } + ) +} + +@Preview(showBackground = true) +@Composable +fun TextFieldComponentPreview() { + TextFieldComponent {} +} + +@Composable +fun AnimalCard(image: Int, selected: Boolean, onImageClicked: (animalName: String) -> Unit) { + val localFocusManager = LocalFocusManager.current + Card( + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .padding(16.dp) + .size(56.dp) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .border( + width = 1.dp, + color = if (selected) Color.Green else Color.Transparent, + shape = RoundedCornerShape(8.dp), + ), + ) { + Image( + modifier = Modifier + .padding(16.dp) + .wrapContentWidth() + .wrapContentHeight() + .clickable { + val animalName = if (image == R.drawable.ic_audiotrack_64) "Cat" else "Dog" + onImageClicked(animalName) + localFocusManager.clearFocus() + }, + painter = painterResource(id = image), + contentDescription = "Animal image", + ) + } + } +} + +@Preview() +@Composable +fun AnimalCardPreview() { + AnimalCard(image = R.drawable.ic_color_lens, false) {} +} + + +@Composable +fun ButtonComponent(onClicked: () -> Unit, text: String) { + Button( + modifier = Modifier + .padding(8.dp) + .wrapContentWidth(), + onClick = { onClicked.invoke() } + ) { + TextComponent(textValue = text, textSize = 18.sp,) + } +} + +@Preview() +@Composable +fun ButtonComponentPreview() { + ButtonComponent(text = "Text", onClicked = {}) +} + +@Composable +fun InfoCard(animalSelected: String?) { + Card( + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .padding(16.dp), + elevation = CardDefaults.cardElevation() + ) { + Text( + modifier = Modifier.padding(18.dp, 24.dp), + text = if (animalSelected == "Dog") "Dog info" else if (animalSelected == "Cat") "Cat info" else "", + fontSize = 18.sp, + color = Color.Black, + fontWeight = FontWeight.Medium, + + ) + } +} + +@Preview() +@Composable +fun InfoCardPreview() { + InfoCard("This is information card to provide some info") +} + +@Composable +fun TitleBar(value: String, onBackPressed: () -> Unit) { +// val localFocusManager = LocalFocusManager.current + Row( + modifier = Modifier + .height(60.dp) + .fillMaxWidth() + .padding(0.dp, 4.dp, 0.dp, 0.dp) + .background(color = MaterialTheme.colorScheme.surface), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + FilledIconButton( + onClick = onBackPressed, + modifier = Modifier + .padding(8.dp) + .align(Alignment.CenterVertically), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) + ) { + Icon( + imageVector = Icons.Default.ArrowBack, + contentDescription = "Navigate back", + modifier = Modifier.size(24.dp) + ) + } + + Text( + text = value, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 24.sp, + fontFamily = FontFamily( + Font( + DeviceFontFamilyName("sans-serif"), + weight = FontWeight.Light + ) + ), + ) + Spacer(modifier = Modifier.weight(1f)) + } +} + +@Preview(showBackground = true) +@Composable +fun TitleBarPreview() { + TitleBar("Title bar", {}) +} + +@Composable +fun InfoItem(label: String, value: String) { + Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) { + Text( + modifier = Modifier + .padding(16.dp, 8.dp, 16.dp, 2.dp) + .wrapContentWidth() + .wrapContentHeight(), + text = label, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 18.sp, + fontFamily = FontFamily( + Font( + DeviceFontFamilyName("sans-serif"), + weight = FontWeight.Light + ) + ), + ) + Text( + modifier = Modifier + .padding(16.dp, 2.dp, 16.dp, 8.dp) + .wrapContentWidth() + .wrapContentHeight(), + text = value, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 22.sp, + fontWeight = FontWeight.Normal + ) + } +} + +@Preview(showBackground = true) +@Composable +fun InfoItemPreview() { + InfoItem("Label", "Value") +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/ComposePlaygroundScreen.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/ComposePlaygroundScreen.kt new file mode 100644 index 000000000..f2826da09 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/ComposePlaygroundScreen.kt @@ -0,0 +1,219 @@ +package com.dimowner.audiorecorder.v2 + +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.dimowner.audiorecorder.ARApplication +import com.dimowner.audiorecorder.R +import com.dimowner.audiorecorder.app.main.MainActivity +import com.dimowner.audiorecorder.util.TimeUtils +import com.dimowner.audiorecorder.v2.info.RecordInfoState +import com.dimowner.audiorecorder.v2.settings.ChipItem +import com.dimowner.audiorecorder.v2.settings.SettingSelector +import com.google.gson.Gson +import timber.log.Timber + +@Composable +fun ComposePlaygroundScreen( + navController: NavHostController, + userInputViewModel: UserInputViewModel, + showWelcomeScreen: (Pair) -> Unit, + showRecordInfoScreen: (String) -> Unit, + showSettingsScreen: () -> Unit +) { + val context = LocalContext.current + + val recordInfo = RecordInfoState( + name = "name666", + format = "format777", + duration = TimeUtils.formatTimeIntervalHourMinSec2(150000000/1000), + size = ARApplication.injector.provideSettingsMapper(context).formatSize(1500000), + location = "location888", + created = TimeUtils.formatDateTimeLocale(System.currentTimeMillis()), + sampleRate = stringResource(R.string.value_hz, 44000), + channelCount = stringResource(R.string.mono), + bitrate = stringResource(R.string.value_kbps, 240000/1000), + ) + + Surface( + modifier = Modifier.fillMaxSize() + ) { + Column(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) + .padding(16.dp) + ) { + TextComponent(textValue = "Name", textSize = 18.sp) + Spacer(modifier = Modifier.size(10.dp)) + TextFieldComponent(onTextChanged = { + userInputViewModel.onEvent(UserDataUiEvents.UserNameEntered(it)) + }) + Spacer(modifier = Modifier.size(10.dp)) + TextComponent(textValue = "What do you like?", textSize = 18.sp) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + AnimalCard(image = R.drawable.ic_audiotrack_64, onImageClicked = { + userInputViewModel.onEvent(UserDataUiEvents.AnimalSelected(it)) + }, selected = userInputViewModel.uiState.value.animalSelected == "Cat") + AnimalCard(image = R.drawable.ic_color_lens, onImageClicked = { + userInputViewModel.onEvent(UserDataUiEvents.AnimalSelected(it)) + }, selected = userInputViewModel.uiState.value.animalSelected == "Dog") + } + Spacer(modifier = Modifier.weight(1f)) + if (userInputViewModel.isValidState()) { + ButtonComponent(onClicked = { + showWelcomeScreen( + Pair( + userInputViewModel.uiState.value.nameEntered, + userInputViewModel.uiState.value.animalSelected + ) + ) + }, text = "Go to details screen") + } + Row { + ButtonComponent(onClicked = { + val json = Uri.encode(Gson().toJson(recordInfo)) + showRecordInfoScreen.invoke(json) + }, text = "Record Info") + ButtonComponent(onClicked = { + showSettingsScreen.invoke() + }, text = "Settings") + } + + // Text variations + Text( + text = "Primary Text", + modifier = Modifier.padding(16.dp), + ) + Text( + text = "Secondary Text", + modifier = Modifier.padding(16.dp), + ) + Text( + text = "Error Text", + modifier = Modifier.padding(16.dp), + ) + + Spacer(modifier = Modifier.size(10.dp)) + + SettingSelector( + name = "Test Name", + chips = listOf( + ChipItem(id = 0, value = 1000, "1000", false), + ChipItem(id = 1, value = 2000, "2000", true), + ChipItem(id = 2, value = 3000, "3000", false), + ChipItem(id = 4, value = 4, "4", false), + ChipItem(id = 5, value = 5, "5", false), + ChipItem(id = 6, value = 600, "600", false), + ChipItem(id = 7, value = 70000, "70000", false), + ), + onSelect = { + Timber.v("MY_TEST: onSelect = " + it.name) + } + ) + // Buttons with different states + Button( + onClick = { context.startActivity(Intent(context, MainActivity::class.java)) }, + colors = ButtonDefaults.buttonColors() + ) { + Text(text = "Audio Recorder",) + } + Button( + onClick = {}, + enabled = false, + colors = ButtonDefaults.buttonColors() + ) { + Text(text = "Disabled Button",) + } + Card(elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)) { + Text( + modifier = Modifier.padding(16.dp), + text = "Elevated Surface", + ) + } + CircularProgressIndicator( + modifier = Modifier.padding(16.dp) + ) + LinearProgressIndicator( + progress = { 0.5f }, + modifier = Modifier.padding(16.dp), + ) + Slider( + value = 0.5f, + onValueChange = {}, + ) + Row(modifier = Modifier.fillMaxSize()) { + Switch( + checked = true, + onCheckedChange = {}, + enabled = true, + modifier = Modifier.padding(16.dp) + ) + Switch( + checked = false, + onCheckedChange = {}, + enabled = true, + modifier = Modifier.padding(16.dp) + ) + Switch( + checked = false, + onCheckedChange = {}, + enabled = false, + modifier = Modifier.padding(16.dp) + ) + } + HorizontalDivider( + modifier = Modifier.padding(16.dp) + ) + BottomAppBar( + content = { + Text( + text = "Bottom App Bar", + ) + } + ) + } + } + } +} + +@Preview +@Composable +fun UserInputScreenPreview() { + ComposePlaygroundScreen(rememberNavController(), viewModel(), {}, {}, {}) +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/HomeActivity.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/HomeActivity.kt similarity index 53% rename from app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/HomeActivity.kt rename to app/src/main/java/com/dimowner/audiorecorder/v2/HomeActivity.kt index cf672bc04..d3a815224 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/HomeActivity.kt +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/HomeActivity.kt @@ -1,11 +1,13 @@ -package com.dimowner.audiorecorder.app.v2.ui +package com.dimowner.audiorecorder.v2 +import android.os.Build import androidx.activity.ComponentActivity import android.os.Bundle import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import com.dimowner.audiorecorder.app.v2.ui.theme.Compose1Theme +import com.dimowner.audiorecorder.v2.navigation.RecorderNavigationGraph +import com.dimowner.audiorecorder.v2.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -15,15 +17,16 @@ class HomeActivity: ComponentActivity() { super.onCreate(savedInstanceState) installSplashScreen() setContent { - Compose1Theme { - // A surface container using the 'background' color from the theme - FunFactsApp() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + AppTheme(dynamicColors = true, darkTheme = true) { RecorderApp() } + } else { + AppTheme(darkTheme = true) { RecorderApp() } } } } @Composable - fun FunFactsApp() { - FunFactsNavigationGraph() + fun RecorderApp() { + RecorderNavigationGraph() } } diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputViewModel.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/UserInputViewModel.kt similarity index 92% rename from app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputViewModel.kt rename to app/src/main/java/com/dimowner/audiorecorder/v2/UserInputViewModel.kt index 2d734b973..5bba915a3 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/UserInputViewModel.kt +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/UserInputViewModel.kt @@ -1,39 +1,39 @@ -package com.dimowner.audiorecorder.app.v2.ui - -import androidx.compose.runtime.mutableStateOf -import androidx.lifecycle.ViewModel - -class UserInputViewModel: ViewModel() { - - val uiState = mutableStateOf(UserInputScreenState()) - - fun onEvent(event: UserDataUiEvents) { - when(event) { - is UserDataUiEvents.UserNameEntered -> { - uiState.value = uiState.value.copy( - nameEntered = event.name - ) - } - is UserDataUiEvents.AnimalSelected -> { - uiState.value = uiState.value.copy( - animalSelected = event.animalValue - ) - } - } - - } - - fun isValidState(): Boolean { - return uiState.value.animalSelected.isNotEmpty() && uiState.value.nameEntered.isNotEmpty() - } -} - -data class UserInputScreenState( - val nameEntered: String = "", - val animalSelected: String = "" -) - -sealed class UserDataUiEvents { - data class UserNameEntered(val name: String): UserDataUiEvents() - data class AnimalSelected(val animalValue: String): UserDataUiEvents() -} +package com.dimowner.audiorecorder.v2 + +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel + +class UserInputViewModel: ViewModel() { + + val uiState = mutableStateOf(UserInputScreenState()) + + fun onEvent(event: UserDataUiEvents) { + when(event) { + is UserDataUiEvents.UserNameEntered -> { + uiState.value = uiState.value.copy( + nameEntered = event.name + ) + } + is UserDataUiEvents.AnimalSelected -> { + uiState.value = uiState.value.copy( + animalSelected = event.animalValue + ) + } + } + + } + + fun isValidState(): Boolean { + return uiState.value.animalSelected.isNotEmpty() && uiState.value.nameEntered.isNotEmpty() + } +} + +data class UserInputScreenState( + val nameEntered: String = "", + val animalSelected: String = "" +) + +sealed class UserDataUiEvents { + data class UserNameEntered(val name: String): UserDataUiEvents() + data class AnimalSelected(val animalValue: String): UserDataUiEvents() +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/WelcomeScreen.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/WelcomeScreen.kt similarity index 90% rename from app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/WelcomeScreen.kt rename to app/src/main/java/com/dimowner/audiorecorder/v2/WelcomeScreen.kt index fc76c6f2f..1bfa6144a 100644 --- a/app/src/main/java/com/dimowner/audiorecorder/app/v2/ui/WelcomeScreen.kt +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/WelcomeScreen.kt @@ -1,44 +1,43 @@ -package com.dimowner.audiorecorder.app.v2.ui - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -@Composable -fun WelcomeScreen(userName: String?, animalSelected: String?) { - Surface( - modifier = Modifier.fillMaxSize() - ) { - Column(modifier = Modifier - .fillMaxSize() - .padding(16.dp)) { - TopBar("Welcome $userName \uD83D\uDE0A") - TextComponent(textValue = "Thank you for sharing your data!", textSize = 24.sp) - Spacer(modifier = Modifier.size(60.dp)) - TextComponent( - textValue = "You are $animalSelected lover!", - textSize = 24.sp, - fontWeight = FontWeight.Normal - ) - Spacer(modifier = Modifier.size(16.dp)) - InfoCard(animalSelected = animalSelected) - - } - } -} - - -@Preview -@Composable -fun WelcomeScreenPreview() { - WelcomeScreen("name", "animal") -} +package com.dimowner.audiorecorder.v2 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun WelcomeScreen(userName: String?, animalSelected: String?) { + Surface( + modifier = Modifier.fillMaxSize() + ) { + Column(modifier = Modifier + .fillMaxSize() + .padding(16.dp)) { + TextComponent(textValue = "Thank you for sharing your data!", textSize = 24.sp) + Spacer(modifier = Modifier.size(60.dp)) + TextComponent( + textValue = "You are $animalSelected lover!", + textSize = 24.sp, + fontWeight = FontWeight.Normal + ) + Spacer(modifier = Modifier.size(16.dp)) + InfoCard(animalSelected = animalSelected) + + } + } +} + + +@Preview +@Composable +fun WelcomeScreenPreview() { + WelcomeScreen("name", "animal") +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/info/AssetParamType.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/info/AssetParamType.kt new file mode 100644 index 000000000..aac40e036 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/info/AssetParamType.kt @@ -0,0 +1,25 @@ +package com.dimowner.audiorecorder.v2.info + +import android.os.Build +import android.os.Bundle +import androidx.navigation.NavType +import com.google.gson.Gson + +class AssetParamType : NavType(isNullableAllowed = false) { + + override fun get(bundle: Bundle, key: String): RecordInfoState? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + bundle.getParcelable(key, RecordInfoState::class.java) + } else { + bundle.getParcelable(key) + } + } + + override fun parseValue(value: String): RecordInfoState { + return Gson().fromJson(value, RecordInfoState::class.java) + } + + override fun put(bundle: Bundle, key: String, value: RecordInfoState) { + bundle.putParcelable(key, value) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoScreen.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoScreen.kt new file mode 100644 index 000000000..562f79a58 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoScreen.kt @@ -0,0 +1,61 @@ +package com.dimowner.audiorecorder.v2.info + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.dimowner.audiorecorder.R +import com.dimowner.audiorecorder.v2.InfoItem +import com.dimowner.audiorecorder.v2.TitleBar + +@Composable +fun RecordInfoScreen( + navController: NavHostController, + recordInfo: RecordInfoState? +) { + + Surface( + modifier = Modifier.fillMaxSize() + ) { + Column(modifier = Modifier.fillMaxSize()) { + TitleBar( + stringResource(id = R.string.info), + onBackPressed = { navController.popBackStack() } + ) + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) + ) { + Spacer(modifier = Modifier.size(8.dp)) + InfoItem(stringResource(R.string.rec_name), recordInfo?.name ?: "") + InfoItem(stringResource(R.string.rec_format), recordInfo?.format ?: "") + InfoItem(stringResource(R.string.bitrate), recordInfo?.bitrate.toString()) + InfoItem(stringResource(R.string.channels), recordInfo?.channelCount.toString()) + InfoItem(stringResource(R.string.sample_rate), recordInfo?.sampleRate.toString()) + InfoItem(stringResource(R.string.rec_duration), recordInfo?.duration.toString()) + InfoItem(stringResource(R.string.rec_size), recordInfo?.size.toString()) + InfoItem(stringResource(R.string.rec_location), recordInfo?.location ?: "") + InfoItem(stringResource(R.string.rec_created), recordInfo?.created.toString()) + Spacer(modifier = Modifier.size(8.dp)) + } + } + } +} + + +@Preview +@Composable +fun RecordInfoScreenPreview() { + RecordInfoScreen(rememberNavController(), null) +} \ No newline at end of file diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoState.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoState.kt new file mode 100644 index 000000000..2175fae7b --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/info/RecordInfoState.kt @@ -0,0 +1,22 @@ +package com.dimowner.audiorecorder.v2.info + +import android.os.Parcelable +import com.dimowner.audiorecorder.AppConstants +import kotlinx.parcelize.Parcelize + +@Parcelize +class RecordInfoState( + val name: String, + val format: String, + val duration: String, + val size: String, + val location: String, + val created: String, + val sampleRate: String, + val channelCount: String, + val bitrate: String, +) : Parcelable { + + val nameWithExtension: String + get() = name + AppConstants.EXTENSION_SEPARATOR + format +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/navigation/RecorerNavigationGraph.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/navigation/RecorerNavigationGraph.kt new file mode 100644 index 000000000..35e974601 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/navigation/RecorerNavigationGraph.kt @@ -0,0 +1,113 @@ +package com.dimowner.audiorecorder.v2.navigation + +import android.os.Build +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.EaseIn +import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavBackStackEntry +import androidx.navigation.NavType +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.navArgument +import com.dimowner.audiorecorder.v2.ComposePlaygroundScreen +import com.dimowner.audiorecorder.v2.UserInputViewModel +import com.dimowner.audiorecorder.v2.WelcomeScreen +import com.dimowner.audiorecorder.v2.info.AssetParamType +import com.dimowner.audiorecorder.v2.info.RecordInfoState +import com.dimowner.audiorecorder.v2.info.RecordInfoScreen +import com.dimowner.audiorecorder.v2.settings.SettingsScreen + +private const val ANIMATION_DURATION = 120 + +@Composable +fun RecorderNavigationGraph(userInputViewModel: UserInputViewModel = viewModel()) { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = Routes.COMPOSE_PLAYGROUND_SCREEN, + enterTransition = { enterTransition(this) }, + exitTransition = { exitTransition(this) }, + popEnterTransition = { popEnterTransition(this) }, + popExitTransition = { popExitTransition(this) } + ) { + composable(Routes.COMPOSE_PLAYGROUND_SCREEN) { + ComposePlaygroundScreen(navController, userInputViewModel, + showWelcomeScreen = { + navController.navigate(Routes.WELCOME_SCREEN +"/${it.first}/${it.second}") + }, + showRecordInfoScreen = { json -> + navController.navigate(Routes.RECORD_INFO_SCREEN +"/${json}") + }, + showSettingsScreen = { + navController.navigate(Routes.SETTINGS_SCREEN) + } + ) + } + composable(Routes.SETTINGS_SCREEN) { + SettingsScreen(navController) + } + composable("${Routes.WELCOME_SCREEN}/{${Routes.USER_NAME}}/{${Routes.ANIMAL_SELECTED}}", + arguments = listOf( + navArgument(name = Routes.USER_NAME) { type = NavType.StringType }, + navArgument(name = Routes.ANIMAL_SELECTED) { type = NavType.StringType } + ), + ) { + val userName = it.arguments?.getString(Routes.USER_NAME) + val animalSelected = it.arguments?.getString(Routes.ANIMAL_SELECTED) + WelcomeScreen(userName = userName, animalSelected = animalSelected) + } + + composable( + "${Routes.RECORD_INFO_SCREEN}/{${Routes.RECORD_INFO}}", + arguments = listOf( + navArgument(Routes.RECORD_INFO) { + type = AssetParamType() + } + ), + ) { + val recordInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + it.arguments?.getParcelable(Routes.RECORD_INFO, RecordInfoState::class.java) + } else { + it.arguments?.getParcelable(Routes.RECORD_INFO) + } + RecordInfoScreen(navController, recordInfo) + } + } +} + +private fun enterTransition(scope: AnimatedContentTransitionScope): EnterTransition { + return scope.slideIntoContainer( + animationSpec = tween(ANIMATION_DURATION, easing = EaseIn), + towards = AnimatedContentTransitionScope.SlideDirection.Start + ) +} + +private fun exitTransition(scope: AnimatedContentTransitionScope): ExitTransition { + return scope.slideOutOfContainer( + animationSpec = tween(ANIMATION_DURATION, easing = EaseIn), + towards = AnimatedContentTransitionScope.SlideDirection.Start + ) +} + +private fun popEnterTransition(scope: AnimatedContentTransitionScope): EnterTransition { + return scope.slideIntoContainer( + animationSpec = tween(ANIMATION_DURATION, easing = EaseOut), + towards = AnimatedContentTransitionScope.SlideDirection.End + ) +} + +private fun popExitTransition(scope: AnimatedContentTransitionScope): ExitTransition { + return scope.slideOutOfContainer( + animationSpec = tween(ANIMATION_DURATION, easing = EaseOut), + towards = AnimatedContentTransitionScope.SlideDirection.End + ) +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/navigation/Routes.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/navigation/Routes.kt new file mode 100644 index 000000000..5dea9c23f --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/navigation/Routes.kt @@ -0,0 +1,26 @@ +package com.dimowner.audiorecorder.v2.navigation + +object Routes { + + //TODO: Screen list + /** + * Recording/Playback + * Settings + * Records list + * Trash + * Record info + * + * Welcome + * Welcome setup + * + * */ + + const val SETTINGS_SCREEN = "SETTINGS_SCREEN" + const val RECORD_INFO_SCREEN = "RECORD_INFO_SCREEN" + const val COMPOSE_PLAYGROUND_SCREEN = "COMPOSE_PLAYGROUND_SCREEN" + const val WELCOME_SCREEN = "WELCOME_SCREEN" + + const val USER_NAME = "NAME" + const val ANIMAL_SELECTED = "ANIMAL_SELECTED" + const val RECORD_INFO = "RECORD_INFO" +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsComponents.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsComponents.kt new file mode 100644 index 000000000..4b0ce35da --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsComponents.kt @@ -0,0 +1,450 @@ +package com.dimowner.audiorecorder.v2.settings + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.DeviceFontFamilyName +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.dimowner.audiorecorder.R + +@Composable +fun SettingsItem( + label: String, + iconRes: Int, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .clickable { onClick() }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .padding(24.dp, 16.dp, 16.dp, 16.dp) + .wrapContentWidth() + .wrapContentHeight(), + painter = painterResource(id = iconRes), + contentDescription = label, + ) + Text( + modifier = Modifier + .padding(0.dp, 12.dp, 0.dp, 12.dp) + .wrapContentWidth() + .wrapContentHeight(), + text = label, + fontSize = 20.sp, + fontFamily = FontFamily( + Font( + DeviceFontFamilyName("sans-serif"), + weight = FontWeight.Light + ) + ), + ) + + } +} + +@Preview(showBackground = true) +@Composable +fun SettingsItemPreview() { + SettingsItem("Label", R.drawable.ic_color_lens, {}) +} + +@Composable +fun SettingsItemCheckBox( + label: String, + iconRes: Int, + onCheckedChange: ((Boolean) -> Unit), +) { + Row( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + modifier = Modifier + .padding(24.dp, 16.dp, 16.dp, 16.dp) + .wrapContentWidth() + .wrapContentHeight(), + painter = painterResource(id = iconRes), + contentDescription = label + ) + Text( + modifier = Modifier + .padding(0.dp, 12.dp, 0.dp, 12.dp) + .weight(1f) + .wrapContentHeight(), + text = label, + fontSize = 20.sp, + fontFamily = FontFamily( + Font( + DeviceFontFamilyName("sans-serif"), + weight = FontWeight.Light + ) + ), + ) + Switch( + checked = true, + onCheckedChange = { onCheckedChange(it) }, + enabled = true, + modifier = Modifier.padding(8.dp) + ) + } +} + +@Preview(showBackground = true) +@Composable +fun SettingsItemCheckBoxPreview() { + SettingsItemCheckBox("Label", R.drawable.ic_color_lens, {}) +} + +@Composable +fun AppInfoView(appName: String, version: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + textAlign = TextAlign.Center, + text = appName, + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 20.sp, + fontFamily = FontFamily( + Font( + DeviceFontFamilyName("sans-serif"), + weight = FontWeight.Medium + ) + ), + ) + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + textAlign = TextAlign.Center, + text = version, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp, + fontWeight = FontWeight.Light + ) + } +} + +@Preview(showBackground = true) +@Composable +fun AppInfoViewPreview() { + AppInfoView("App Name", "App Version") +} + +@Composable +fun InfoTextView(value: String) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp, 0.dp, 16.dp, 0.dp), + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + text = value, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp, + fontWeight = FontWeight.Light + ) + } +} + +@Preview(showBackground = true) +@Composable +fun InfoTextViewPreview() { + InfoTextView("InfoTextView") +} + +@Composable +fun ResetRecordingSettingsPanel( + text: String, + onClick: () -> Unit, +) { + Card( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(4.dp), + ) { + Row( + modifier = Modifier.fillMaxSize(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier + .weight(1f) + .wrapContentHeight() + .padding(8.dp), + textAlign = TextAlign.Start, + text = text, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 16.sp, + fontWeight = FontWeight.Light + ) + Button( + modifier = Modifier + .padding(8.dp) + .wrapContentSize(), + + onClick = { onClick() } + ) { + Text( + text = stringResource(id = R.string.btn_reset), + fontSize = 18.sp, + fontWeight = FontWeight.Light, + ) + } + } + + } +} + +@Preview(showBackground = true) +@Composable +fun ResetRecordingSettingsPanelPreview() { + ResetRecordingSettingsPanel("ResetRecordingSettingsPanel", {}) +} + +@Composable +fun SettingSelector( + name: String, + chips: List, + onSelect: (ChipItem) -> Unit, +) { +// val screenWidth = LocalConfiguration.current.screenWidthDp.dp.value +// var grid = calculateChipsPositions(values, screenWidth) + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + ) { + Text( + modifier = Modifier + .wrapContentSize() + .padding(4.dp), + textAlign = TextAlign.Start, + text = name, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 16.sp, + fontWeight = FontWeight.Light + ) + ChipsPanel( + modifier = Modifier.wrapContentSize(), + chips = chips, + onSelect = onSelect + ) +// var k = 0 +// grid.forEach { item -> +// Timber.v("MY_TEST createRows") +// Row( +// modifier = Modifier +// .fillMaxWidth() +// .wrapContentHeight(), +// verticalAlignment = Alignment.CenterVertically, +// ) { +// for (j in 0..< item.value) { +// Timber.v("MY_TEST create Chips j = " + j) +// ChipComponent( +// modifier = Modifier, +// values[k], +// onSelect +// ) +// k++ +// } +// } +// } + } +} + +@Composable +fun ChipComponent( + modifier: Modifier = Modifier, + item: ChipItem, + onSelect: (ChipItem) -> Unit, +) { + Card( + modifier = modifier + .wrapContentSize() + .padding(2.dp, 0.dp), + shape = RoundedCornerShape(18.dp), + border = if (item.isSelected) { + BorderStroke(2.dp, MaterialTheme.colorScheme.primary) + } else { null }, + onClick = { onSelect(item) } + ) { + Row( + modifier = Modifier + .wrapContentSize() + .padding(if (item.isSelected) 12.dp else 25.dp, 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (item.isSelected) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = item.name, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier + .size(29.dp) + .padding(0.dp, 3.dp, 3.dp, 3.dp) + .align(Alignment.CenterVertically) + ) + } + Text( + modifier = Modifier + .wrapContentSize() + .padding(2.dp), + textAlign = TextAlign.Start, + text = item.name, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 17.sp, + fontWeight = FontWeight.Normal + ) + } + } + +} + +//@Composable +//fun calculateChipsPositions(chips: List, screenWidth: Float): Map { +// val map = mutableMapOf() +// var row = 0 +// var col = 0 +// var cumulativeWidth = 0f +// val textStyle = TextStyle( +// fontSize = 17.sp, +// fontWeight = FontWeight.Normal, +// textAlign = TextAlign.Start +// ) +// chips.forEach { chip -> +// val chipWidthExtra = 58 //Dp +// val chipWidth = measureTextWidth(chip.name, textStyle).value + chipWidthExtra.dp.value +// cumulativeWidth += chipWidth +// if (cumulativeWidth > screenWidth) { +// map[row] = col +// row++ +// col = 1 +// cumulativeWidth = chipWidth +// } else { +// col++ +// map[row] = col +// } +// } +// return map +//} + +@Composable +fun measureTextWidth(text: String, style: TextStyle): Dp { + val textMeasurer = rememberTextMeasurer() + val widthInPixels = textMeasurer.measure(text, style).size.width + return with(LocalDensity.current) { widthInPixels.toDp() } +} + +@Preview(showBackground = true) +@Composable +fun SettingSelectorPreview() { + SettingSelector("SettingsSelector", emptyList(), {}) +} + +@Composable +fun ChipsPanel( + modifier: Modifier = Modifier, + chips: List, + onSelect: (ChipItem) -> Unit, +) { + Layout( + modifier = modifier, + content = { chips.map { + ChipComponent(modifier = Modifier.wrapContentSize(), item = it, onSelect = onSelect) } + } + ) { measurables, constraints -> + // List of measured children + val placeables = measurables.map { measurable -> + // Measure each children + measurable.measure(constraints) + } + val totalHeight = calculatePositionsDefault(placeables, constraints.maxWidth) + + // Set the size of the layout as big as it can + layout(constraints.maxWidth, totalHeight) { + calculatePositionsDefault( + temp = placeables, + constraints.maxWidth, + ) { placeable, x, y -> placeable.place(x, y) } + } + } +} + +fun calculatePositionsDefault( + temp: List, + viewWidth: Int, + onPlace: ((Placeable, x: Int, y: Int) -> Unit)? = null +): Int { + val rowHeight = temp.first().measuredHeight + var rowCount = 0 + var posY = 0 + var posX = 0 + rowCount++ + if (temp.isNotEmpty()) { + var availableWidth = viewWidth + for (i in temp.indices) { + if (availableWidth < temp[i].measuredWidth) { + rowCount++ + posY += rowHeight + availableWidth = viewWidth + posX = 0 + } + onPlace?.invoke(temp[i], posX, posY) + val width: Int = (temp[i].measuredWidth) + posX += width + availableWidth -= width + } + posY += rowHeight + } + return posY +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsScreen.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsScreen.kt new file mode 100644 index 000000000..6fff84f6a --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsScreen.kt @@ -0,0 +1,122 @@ +package com.dimowner.audiorecorder.v2.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController +import com.dimowner.audiorecorder.R +import com.dimowner.audiorecorder.v2.TitleBar +import timber.log.Timber + +@Composable +fun SettingsScreen( + navController: NavHostController, +) { + Surface( + modifier = Modifier.fillMaxSize() + ) { + Column(modifier = Modifier.fillMaxSize()) { + TitleBar( + stringResource(R.string.settings), + onBackPressed = { + navController.popBackStack() + }) + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .weight(weight = 1f, fill = false) + ) { + Spacer(modifier = Modifier.size(8.dp)) + SettingsItem(stringResource(R.string.trash), R.drawable.ic_delete, { + + }) + SettingsItem(stringResource(R.string.app_name), R.drawable.ic_color_lens, { + + }) + SettingsItemCheckBox(stringResource(R.string.keep_screen_on), R.drawable.ic_lightbulb_on, { + + }) + SettingsItemCheckBox(stringResource(R.string.ask_to_rename), R.drawable.ic_pencil, { + + }) + SettingsItem(stringResource(R.string.rec_format), R.drawable.ic_title, { + + }) + ResetRecordingSettingsPanel("1 Mb/min expected size\nM4a, 44.1kHz, 256kbps, Stereo", { + + }) + val formats = stringArrayResource(id = R.array.formats2).toList() + SettingSelector( + name = stringResource(id = R.string.recording_format), + chips = formats.mapIndexed { index, format -> + ChipItem(id = index, value = index, format, false) + }, + onSelect = { + Timber.v("MY_TEST: onSelect = " + it.name) + } + ) + val sampleRates = stringArrayResource(id = R.array.sample_rates2).toList() + SettingSelector( + name = stringResource(id = R.string.sample_rate), + chips = sampleRates.mapIndexed { index, rate -> + ChipItem(id = index, value = index, rate, false) + }, + onSelect = { + Timber.v("MY_TEST: onSelect = " + it.name) + } + ) + val bitRates = stringArrayResource(id = R.array.bit_rates2).toList() + SettingSelector( + name = stringResource(id = R.string.bitrate), + chips = bitRates.mapIndexed { index, rate -> + ChipItem(id = index, value = index, rate, false) + }, + onSelect = { + Timber.v("MY_TEST: onSelect = " + it.name) + } + ) + val channels = stringArrayResource(id = R.array.channels).toList() + SettingSelector( + name = stringResource(id = R.string.channels), + chips = channels.mapIndexed { index, channel -> + ChipItem(id = index, value = index, channel, false) + }, + onSelect = { + Timber.v("MY_TEST: onSelect = " + it.name) + } + ) + Spacer(modifier = Modifier.size(8.dp)) + SettingsItem(stringResource(R.string.rate_app), R.drawable.ic_thumbs, { + + }) + SettingsItem(stringResource(R.string.request), R.drawable.ic_chat_bubble, { + + }) + Spacer(modifier = Modifier.size(8.dp)) + InfoTextView("InfoTextView") + InfoTextView("InfoTextView") + InfoTextView("InfoTextView") + AppInfoView(stringResource(id = R.string.app_name), stringResource(id = R.string.app_name)) + Spacer(modifier = Modifier.size(8.dp)) + } + } + } +} + + +@Preview +@Composable +fun RecordInfoScreenPreview() { + SettingsScreen(rememberNavController()) +} \ No newline at end of file diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsState.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsState.kt new file mode 100644 index 000000000..26b1b5c02 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/settings/SettingsState.kt @@ -0,0 +1,41 @@ +package com.dimowner.audiorecorder.v2.settings + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +class SettingsState( + val isDynamicColors: Boolean, + val isDarkTheme: Boolean, + val isKeepScreenOn: Boolean, + val isShowRenameDialog: Boolean, + val namingFormat: String, + val recordingSetting: RecordingSetting, + val rateAppLink: String, + val feedbackEmail: String, + val totalRecordCount: Long, + val totalRecordDuration: Long, + val availableSpace: Long, + val appName: String, + val appVersion: String, +) : Parcelable + +@Parcelize +data class RecordingSetting( + val formatName: String, + val recordingFormats: List, + val sampleRates: List, + val bitRates: List, + val channelCounts: List, + val selectedSampleRate: Int, + val selectedBitRate: Int, + val selectedChannelCount: Int, +) : Parcelable + +@Parcelize +data class ChipItem( + val id: Int, + val value: Int, + val name: String, + val isSelected: Boolean +) : Parcelable diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Color.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Color.kt new file mode 100644 index 000000000..b1552b496 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Color.kt @@ -0,0 +1,92 @@ +package com.dimowner.audiorecorder.v2.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val BlueGray700 = Color(0xFF1C2733) +val BlueGray500 = Color(0xFF253343) +val WhiteTransparent88 = Color(0x1EFFFFFF) +val BlackTransparent88 = Color(0x1E000000) + +val Pink500 = Color(0xFFE91E63) +val Purple500 = Color(0xFF9C27B0) +val DeepPurple500 = Color(0xFF673AB7) +val Blue500 = Color(0xFF5677FC) +val Teal500 = Color(0xFF009688) +val Green500 = Color(0xFF4CAF50) +val YellowA700 = Color(0xFFFFD600) +val Amber800 = Color(0xFFFF8F00) +val DeepOrange500 = Color(0xFFFF5722) +val Brown500 = Color(0xFF795548) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) + +//val GreyLight = Color(0xFFBBBBBB) +//val BlackTransparent80 = Color(0x32000000) + +val md_theme_light_primary = Color(0xFF00629F) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFD0E4FF) +val md_theme_light_onPrimaryContainer = Color(0xFF001D34) +val md_theme_light_secondary = Color(0xFF00629F) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFD0E4FF) +val md_theme_light_onSecondaryContainer = Color(0xFF001D34) +val md_theme_light_tertiary = Color(0xFF9C3C61) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFFFD9E2) +val md_theme_light_onTertiaryContainer = Color(0xFF3E001D) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFFAFCFF) +val md_theme_light_onBackground = Color(0xFF001F2A) +val md_theme_light_surface = Color(0xFFFAFCFF) +val md_theme_light_onSurface = Color(0xFF001F2A) +val md_theme_light_surfaceVariant = Color(0x1E000000) +val md_theme_light_onSurfaceVariant = Color(0xFF42474E) +val md_theme_light_outline = Color(0xFF73777F) +val md_theme_light_inverseOnSurface = Color(0xFFE1F4FF) +val md_theme_light_inverseSurface = Color(0xFF003547) +val md_theme_light_inversePrimary = Color(0xFF9ACBFF) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF00629F) +val md_theme_light_outlineVariant = Color(0xFFC2C7CF) +val md_theme_light_scrim = Color(0xFF000000) + +val md_theme_dark_primary = Color(0xFF9ACBFF) +val md_theme_dark_onPrimary = Color(0xFF003355) +val md_theme_dark_primaryContainer = Color(0xFF004A79) +val md_theme_dark_onPrimaryContainer = Color(0xFFD0E4FF) +val md_theme_dark_secondary = Color(0xFF9ACBFF) +val md_theme_dark_onSecondary = Color(0xFF003355) +val md_theme_dark_secondaryContainer = Color(0xFF004A79) +val md_theme_dark_onSecondaryContainer = Color(0xFFD0E4FF) +val md_theme_dark_tertiary = Color(0xFFFFB0C8) +val md_theme_dark_onTertiary = Color(0xFF610A33) +val md_theme_dark_tertiaryContainer = Color(0xFF7E244A) +val md_theme_dark_onTertiaryContainer = Color(0xFFFFD9E2) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF001F2A) +val md_theme_dark_onBackground = Color(0xFFBFE9FF) +val md_theme_dark_surface = Color(0xFF001F2A) +val md_theme_dark_onSurface = Color(0xFFBFE9FF) +val md_theme_dark_surfaceVariant = Color(0x1EFFFFFF) +val md_theme_dark_onSurfaceVariant = Color(0xFFC2C7CF) +val md_theme_dark_outline = Color(0xFF8C9199) +val md_theme_dark_inverseOnSurface = Color(0xFF001F2A) +val md_theme_dark_inverseSurface = Color(0xFFBFE9FF) +val md_theme_dark_inversePrimary = Color(0xFF00629F) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFF9ACBFF) +val md_theme_dark_outlineVariant = Color(0xFF42474E) +val md_theme_dark_scrim = Color(0xFF000000) diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Shapes.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Shapes.kt new file mode 100644 index 000000000..6a21c3399 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Shapes.kt @@ -0,0 +1,13 @@ +package com.dimowner.audiorecorder.v2.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val shapes = Shapes( + extraSmall = RoundedCornerShape(4.dp), + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(16.dp), + large = RoundedCornerShape(24.dp), + extraLarge = RoundedCornerShape(32.dp) +) diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Theme.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Theme.kt new file mode 100644 index 000000000..a4e4c9580 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Theme.kt @@ -0,0 +1,188 @@ +package com.dimowner.audiorecorder.v2.theme + +import android.app.Activity +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + + +private val DarkColorScheme = darkColorScheme( + primary = BlueGray700, + secondary = Color.Red, + tertiary = Pink80, + background = BlueGray700, + onPrimary = Pink500, + primaryContainer = Purple500, + onPrimaryContainer = DeepPurple500, + inversePrimary = Blue500, + onSecondary = Teal500, + secondaryContainer = Green500, + onSecondaryContainer = YellowA700, + onTertiary = Amber800, + tertiaryContainer = DeepOrange500, + onTertiaryContainer = Brown500, + onBackground = Pink500, + surface = Purple500, + onSurface = DeepPurple500, + surfaceVariant = Blue500, + onSurfaceVariant = Teal500, + surfaceTint = Green500, + inverseSurface = YellowA700, + inverseOnSurface = Amber800, + error = DeepOrange500, + onError = Brown500, + errorContainer = Pink500, + onErrorContainer = Purple500, + outline = DeepPurple500, + outlineVariant = Blue500, + scrim = Teal500, + surfaceBright = Green500, + surfaceContainer = YellowA700, + surfaceContainerHigh = Amber800, + surfaceContainerHighest = DeepOrange500, + surfaceContainerLow = Brown500, + surfaceContainerLowest = Pink500, + surfaceDim = Purple500, +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40, + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +// Material 3 color schemes +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, + outlineVariant = md_theme_light_outlineVariant, + scrim = md_theme_light_scrim, +) + +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, + outlineVariant = md_theme_dark_outlineVariant, + scrim = md_theme_dark_scrim, +) + +@Composable +@RequiresApi(Build.VERSION_CODES.S) +fun AppTheme( + dynamicColors: Boolean, + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val context = LocalContext.current + val colors = when { + dynamicColors && darkTheme -> dynamicDarkColorScheme(context) + dynamicColors && !darkTheme -> dynamicLightColorScheme(context) + !dynamicColors && !darkTheme -> LightColors + else -> DarkColors + } + AppTheme(colors, darkTheme, content) +} + +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) DarkColors else LightColors + AppTheme(colors, darkTheme, content) +} + +@Composable +private fun AppTheme( + colors: ColorScheme, + darkTheme: Boolean, + content: @Composable () -> Unit +) { + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = colors.primary.toArgb() + WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme + } + } + MaterialTheme( + colorScheme = colors, + typography = typography, + shapes = shapes, + content = content + ) +} diff --git a/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Type.kt b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Type.kt new file mode 100644 index 000000000..ae8aa9903 --- /dev/null +++ b/app/src/main/java/com/dimowner/audiorecorder/v2/theme/Type.kt @@ -0,0 +1,41 @@ +package com.dimowner.audiorecorder.v2.theme + + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Material 3 typography +val typography = Typography( + headlineSmall = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp + ), + titleLarge = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 18.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp + ), + bodyLarge = TextStyle( + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + bodyMedium = TextStyle( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp + ), + labelMedium = TextStyle( + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) +) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c8fe627d7..366a8ae74 100755 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -16,6 +16,8 @@ +