diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts deleted file mode 100644 index e083eef..0000000 --- a/app/build.gradle.kts +++ /dev/null @@ -1,113 +0,0 @@ -import org.jetbrains.kotlin.kapt3.base.Kapt.kapt - -plugins { - id("com.android.application") - id("org.jetbrains.kotlin.android") - id("kotlin-kapt") - id("io.sentry.android.gradle") version "3.11.1" -} - -android { - compileSdk = 33 - - defaultConfig { - applicationId = "io.github.gelassen.wordsinmemory.beta" - minSdk = 21 - targetSdk = 33 - versionCode = 9 - versionName = "1.5.2" - - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - - javaCompileOptions { - annotationProcessorOptions { - arguments["room.schemaLocation"] = - "$projectDir/schemas" - } - } - } - - buildTypes { - getByName("release") { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" - } - buildFeatures { - viewBinding = true - dataBinding = true - } - testOptions { - animationsDisabled = false - } - namespace = "io.github.gelassen.wordinmemory" -} - -dependencies { - -// implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation(files("$projectDir/libs/pipinyin-0.9.1.jar")) - - implementation("androidx.core:core-ktx:1.10.1") - implementation("androidx.appcompat:appcompat:1.6.1") - - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") - implementation("androidx.room:room-runtime:2.5.2") - implementation("androidx.room:room-ktx:2.5.2") - implementation("androidx.work:work-runtime:2.8.1") - implementation("androidx.work:work-runtime-ktx:2.8.1") - implementation("androidx.databinding:databinding-runtime:8.1.0") - implementation("androidx.preference:preference:1.2.1") - - implementation("com.google.mlkit:translate:17.0.1") -// implementation("com.google.mlkit:text-recognition-chinese:16.0.0") -// implementation("com.google.android.gms:play-services-mlkit-text-recognition-chinese:16.0.0") - implementation("com.google.mlkit:translate:17.0.1") - implementation("com.google.android.gms:play-services-mlkit-language-id:17.0.0") - - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") - - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") - implementation("com.squareup.okhttp3:logging-interceptor:4.10.0") - - implementation("com.google.android.material:material:1.9.0") - implementation("de.siegmar:fastcsv:2.2.2") - /* Sentry */ - implementation("io.sentry:sentry-android:6.25.0") { - exclude(group = "androidx.lifecycle", module = "lifecycle-process") - exclude(group = "androidx.lifecycle", module = "lifecycle-common-java8") - } - /* DI */ - implementation("com.google.dagger:dagger:2.42") - implementation("com.google.dagger:dagger-android-support:2.42") - - annotationProcessor("androidx.room:room-compiler:2.5.2") - - kapt("androidx.room:room-compiler:2.5.2") - kapt("com.google.dagger:dagger-compiler:2.42") - - /* kapt android tests */ - kaptAndroidTest("com.google.dagger:dagger-compiler:2.42") - - testImplementation("junit:junit:4.13.2") - testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4") - testImplementation("androidx.arch.core:core-testing:2.1.0") - testImplementation("org.mockito:mockito-inline:3.12.4") - testImplementation("org.mockito:mockito-core:3.12.4") - - androidTestImplementation("androidx.test.ext:junit:1.1.4") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") - androidTestImplementation( "androidx.test.espresso:espresso-contrib:3.5.0") -} diff --git a/app/libs/pipinyin-0.9.1.jar b/app/libs/pipinyin-0.9.1.jar deleted file mode 100644 index 38ca9c2..0000000 Binary files a/app/libs/pipinyin-0.9.1.jar and /dev/null differ diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index ff59496..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/release/app-release.aab b/app/release/app-release.aab deleted file mode 100644 index 8a062bf..0000000 Binary files a/app/release/app-release.aab and /dev/null differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json deleted file mode 100644 index 1914ce2..0000000 --- a/app/release/output-metadata.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 3, - "artifactType": { - "type": "APK", - "kind": "Directory" - }, - "applicationId": "io.github.gelassen.wordsinmemory", - "variantName": "release", - "elements": [ - { - "type": "SINGLE", - "filters": [], - "attributes": [], - "versionCode": 8, - "versionName": "1.5.1", - "outputFile": "app-release.apk" - } - ], - "elementType": "File" -} \ No newline at end of file diff --git a/app/schemas/io.github.gelassen.wordinmemory.storage.AppDatabase/5.json b/app/schemas/io.github.gelassen.wordinmemory.storage.AppDatabase/5.json deleted file mode 100644 index 7223962..0000000 --- a/app/schemas/io.github.gelassen.wordinmemory.storage.AppDatabase/5.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 5, - "identityHash": "cb9a3ffd4d5cf11b7c1bd4c0119d8929", - "entities": [ - { - "tableName": "Subjects", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `data` TEXT NOT NULL, `translation` TEXT NOT NULL, `completed` INTEGER NOT NULL)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subjectToTranslate", - "columnName": "data", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "translation", - "columnName": "translation", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isCompleted", - "columnName": "completed", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb9a3ffd4d5cf11b7c1bd4c0119d8929')" - ] - } -} \ No newline at end of file diff --git a/app/schemas/io.github.gelassen.wordinmemory.storage.AppDatabase/6.json b/app/schemas/io.github.gelassen.wordinmemory.storage.AppDatabase/6.json deleted file mode 100644 index c55d432..0000000 --- a/app/schemas/io.github.gelassen.wordinmemory.storage.AppDatabase/6.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 6, - "identityHash": "4ee90c8e8bd211fb630dc76de659d5c6", - "entities": [ - { - "tableName": "Subjects", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `data` TEXT NOT NULL, `translation` TEXT NOT NULL, `completed` INTEGER NOT NULL, `tutorCounter` INTEGER NOT NULL DEFAULT 0)", - "fields": [ - { - "fieldPath": "uid", - "columnName": "uid", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "subjectToTranslate", - "columnName": "data", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "translation", - "columnName": "translation", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "isCompleted", - "columnName": "completed", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "tutorCounter", - "columnName": "tutorCounter", - "affinity": "INTEGER", - "notNull": true, - "defaultValue": "0" - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "uid" - ] - }, - "indices": [], - "foreignKeys": [] - } - ], - "views": [], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4ee90c8e8bd211fb630dc76de659d5c6')" - ] - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/gelassen/wordinmemory/BaseTest.kt b/app/src/androidTest/java/io/github/gelassen/wordinmemory/BaseTest.kt deleted file mode 100644 index 0dd0ac9..0000000 --- a/app/src/androidTest/java/io/github/gelassen/wordinmemory/BaseTest.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.gelassen.wordinmemory - -import android.content.Context -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.After -import org.junit.Before - -open class BaseTest { - - protected lateinit var appContext: Context - - @Before - open fun setUp() { - // implement custom test runner https://developer.android.com/codelabs/android-dagger#13 - appContext = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext - } - - @After - open fun tearDown() { - // no op - } - -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/gelassen/wordinmemory/DashboardTest.kt b/app/src/androidTest/java/io/github/gelassen/wordinmemory/DashboardTest.kt deleted file mode 100644 index 4293464..0000000 --- a/app/src/androidTest/java/io/github/gelassen/wordinmemory/DashboardTest.kt +++ /dev/null @@ -1,203 +0,0 @@ -package io.github.gelassen.wordinmemory - -import androidx.test.core.app.ActivityScenario -import androidx.test.espresso.IdlingRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.LargeTest -import io.github.gelassen.wordinmemory.idlingresource.DataBindingIdlingResource -import io.github.gelassen.wordinmemory.idlingresource.monitorActivity -import io.github.gelassen.wordinmemory.robots.DashboardRobot -import io.github.gelassen.wordinmemory.storage.AppDatabase -import io.github.gelassen.wordinmemory.ui.MainActivity -import org.junit.After -import org.junit.Ignore -import org.junit.Test -import org.junit.runner.RunWith - -@LargeTest -@RunWith(AndroidJUnit4::class) -class DashboardTest : BaseTest() { - - private val dataBindingIdlingResource = DataBindingIdlingResource() - - private val dashboardRobot: DashboardRobot = DashboardRobot() - - override fun setUp() { - super.setUp() - IdlingRegistry.getInstance().register(dataBindingIdlingResource) - } - - @After - override fun tearDown() { - super.tearDown() - IdlingRegistry.getInstance().unregister(dataBindingIdlingResource) - - AppDatabase.getInstance(appContext).subjectToStudyDao().clean() - } - - @Test - fun onAppStart_noContent_toolbarFabNoContentPlaceholderAreShown() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - - dashboardRobot - .seesToolbar() - .seesFloatingActionButton() - .seesNoContent() - .seesNoContentPlaceholder() - - activityScenario.close() - } - - @Test - fun onFabTap_noContentYet_showAddNewItemDialog() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - - dashboardRobot - .clickOnFabButton() - .seesAddNewItemDialog() - - activityScenario.close() - } - - @Test - fun onFabTap_enterContentAndSave_newItemIsVisible() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - val testTxt = "伦敦是大不列颠的首都。" - val testTranslationTxt = "Lúndūn shì dàbùlièdiān de shǒudū. / Лондон - столица Великобритании." - generateSingleItem(testTxt, testTranslationTxt) - - dashboardRobot - .seesListItemWithText(0, testTxt) - .doesNotSeeNoContentPlaceholder() - .doesNotSeeAddNewItemDialog() - - activityScenario.close() - } - - @Ignore("Scroll is executes fine, but there is no response from toolbar and fab despite on " + - "animation has not been disabled" + - "Test fails, but actual code work well. Fix it when you will have more time.") - @Test - fun onScrollContent_hasContent_hideToolbarAndFab() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - - generateContent() - - dashboardRobot - .scrollToTheFirst() - .scrollToTheLatest() - .doesNotSeeToolbar() - .doesNotSeeFloatingActionButton() - - activityScenario.close() - } - - @Test - fun onClickItem_default_showTranslation() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - val testTxt = "伦敦是大不列颠的首都。" - val testTranslationTxt = "Lúndūn shì dàbùlièdiān de shǒudū. / Лондон - столица Великобритании." - generateSingleItem(testTxt, testTranslationTxt) - - dashboardRobot - .clickOnSubjectToStudy(0, testTxt) - .seesTranslationOfSubject(testTxt + " / " + testTranslationTxt) - - activityScenario.close() - } - - @Test - fun onClickFilterItem_oneItemMarkedAsCompleted_showAllItemsWithoutCompletedOne() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - generateContent(count = 2) - - dashboardRobot - .markSubjectAsCompleted(position = 0) - .seesSpecificNumbersOfItemsInList(count = 1) - - activityScenario.close() - } - - @Test - fun onClickShowAll_twoItemsAreCompleted_showAllItems() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - val totalCount = 5 - generateContent(count = totalCount) - dashboardRobot.markSubjectAsCompleted(position = 0) - dashboardRobot.markSubjectAsCompleted(position = 1) // actually we can again select item at index 0 - - dashboardRobot.clickMenuShowAll() - dashboardRobot.seesSpecificNumbersOfItemsInList(count = totalCount) - - activityScenario.close() - } - - @Test - fun integrationTest_fromNoContentToContentWithCompletedItems_showCorrectStateOnEachStage() { - val activityScenario = ActivityScenario.launch(MainActivity::class.java) - dataBindingIdlingResource.monitorActivity(activityScenario) - - dashboardRobot - .seesToolbar() - .seesFloatingActionButton() - .seesNoContent() - .seesNoContentPlaceholder() - - val testTxt = "伦敦是大不列颠的首都。" - val testTranslationTxt = "Lúndūn shì dàbùlièdiān de shǒudū. / Лондон - столица Великобритании." - generateSingleItem(testTxt, testTranslationTxt) - dashboardRobot - .seesListItemWithText(0, testTxt) - .doesNotSeeNoContentPlaceholder() - .doesNotSeeAddNewItemDialog() - .clickOnSubjectToStudy(0, testTxt) - .seesTranslationOfSubject(testTxt + " / " + testTranslationTxt) - - dashboardRobot - .seesSpecificNumbersOfItemsInList(count = 1) - .markSubjectAsCompleted(position = 0) - .seesSpecificNumbersOfItemsInList(count = 0) - - val totalCountToGenerate = 5 - val totalCount = totalCountToGenerate + 1 // on the previous stage one item has been generated already - val totalCompleteItems = 3 // previous item is also counted - generateContent(count = totalCountToGenerate) - dashboardRobot - .markSubjectAsCompleted(position = 0) - .markSubjectAsCompleted(position = 1) // actually we can again select item at index 0 - .clickMenuShowAll() - .seesSpecificNumbersOfItemsInList(count = totalCount) - .clickMenuShowCompletedOnly() - .seesSpecificNumbersOfItemsInList(count = totalCount - totalCompleteItems) - - activityScenario.close() - } - - private fun generateContent(count: Int = 16) { - for (idx in 0 until count) { - val testTxt = "${idx} 伦敦是大不列颠的首都。" - val testTranslationTxt = "${idx} 伦敦是大不列颠的首都。 / ${idx} Лондон - столица Великобритании." - generateSingleItem(testTxt, testTranslationTxt) - } - } - - private fun generateSingleItem(testTxt: String, testTranslationTxt: String) { - dashboardRobot - .clickOnFabButton() - dashboardRobot - .seesAddNewItemDialog() - .enterNewWord(testTxt, testTranslationTxt) - dashboardRobot - .saveNewWord() - } - - // TODO add integration test - -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/gelassen/wordinmemory/ExampleInstrumentedTest.kt b/app/src/androidTest/java/io/github/gelassen/wordinmemory/ExampleInstrumentedTest.kt deleted file mode 100644 index bae2d59..0000000 --- a/app/src/androidTest/java/io/github/gelassen/wordinmemory/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.gelassen.wordinmemory - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("io.github.gelassen.workinmemory", appContext.packageName) - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/gelassen/wordinmemory/idlingresource/DataBindingIdlingResource.kt b/app/src/androidTest/java/io/github/gelassen/wordinmemory/idlingresource/DataBindingIdlingResource.kt deleted file mode 100644 index c13667b..0000000 --- a/app/src/androidTest/java/io/github/gelassen/wordinmemory/idlingresource/DataBindingIdlingResource.kt +++ /dev/null @@ -1,91 +0,0 @@ -package io.github.gelassen.wordinmemory.idlingresource - -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.fragment.app.FragmentActivity -import androidx.test.core.app.ActivityScenario -import androidx.test.espresso.IdlingResource -import java.util.* - -class DataBindingIdlingResource : IdlingResource { - // List of registered callbacks - private val idlingCallbacks = mutableListOf() - // Give it a unique id to work around an Espresso bug where you cannot register/unregister - // an idling resource with the same name. - private val id = UUID.randomUUID().toString() - // Holds whether isIdle was called and the result was false. We track this to avoid calling - // onTransitionToIdle callbacks if Espresso never thought we were idle in the first place. - private var wasNotIdle = false - - lateinit var activity: FragmentActivity - - override fun getName() = "DataBinding $id" - - override fun isIdleNow(): Boolean { - val idle = !getBindings().any { it.hasPendingBindings() } - @Suppress("LiftReturnOrAssignment") - if (idle) { - if (wasNotIdle) { - // Notify observers to avoid Espresso race detector. - idlingCallbacks.forEach { it.onTransitionToIdle() } - } - wasNotIdle = false - } else { - wasNotIdle = true - // Check next frame. - activity.findViewById(android.R.id.content).postDelayed({ - isIdleNow - }, 16) - } - return idle - } - - override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback) { - idlingCallbacks.add(callback) - } - - /** - * Find all binding classes in all currently available fragments. - */ - private fun getBindings(): List { - val fragments = (activity as? FragmentActivity) - ?.supportFragmentManager - ?.fragments - - val bindings = - fragments?.mapNotNull { - it.view?.getBinding() - } ?: emptyList() - val childrenBindings = fragments?.flatMap { it.childFragmentManager.fragments } - ?.mapNotNull { it.view?.getBinding() } ?: emptyList() - - return bindings + childrenBindings - } -} - -private fun View.getBinding(): ViewDataBinding? = DataBindingUtil.getBinding(this) - -/** - * Sets the activity from an [ActivityScenario] to be used from [DataBindingIdlingResource]. - */ -fun DataBindingIdlingResource.monitorActivity( - activityScenario: ActivityScenario -) { - activityScenario.onActivity { - this.activity = it - } -} - - - -/** - * Sets the fragment from a [FragmentScenario] to be used from [DataBindingIdlingResource]. - */ -//fun DataBindingIdlingResource.monitorFragment(fragmentScenario: FragmentScenario) { -// fragmentScenario.onFragment { it -> -// this.activity = it.requireActivity() -// } -//} - diff --git a/app/src/androidTest/java/io/github/gelassen/wordinmemory/matchers/CustomMatchers.kt b/app/src/androidTest/java/io/github/gelassen/wordinmemory/matchers/CustomMatchers.kt deleted file mode 100644 index 8041702..0000000 --- a/app/src/androidTest/java/io/github/gelassen/wordinmemory/matchers/CustomMatchers.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.gelassen.wordinmemory.matchers - -import android.util.Log -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.matcher.BoundedMatcher -import io.github.gelassen.wordinmemory.App -import org.hamcrest.Description -import org.hamcrest.Matcher - -class CustomMatchers { - - fun recyclerViewSizeMatch(matcherSize: Int): Matcher? { - return object : BoundedMatcher(RecyclerView::class.java) { - override fun describeTo(description: Description) { - description.appendText("RecyclerView with list size: $matcherSize") - } - - override fun matchesSafely(recyclerView: RecyclerView): Boolean { - Log.d("Test", "Item count ${recyclerView.adapter!!.itemCount}") - return matcherSize == recyclerView.adapter!!.itemCount - } - } - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/gelassen/wordinmemory/robots/DashboardRobot.kt b/app/src/androidTest/java/io/github/gelassen/wordinmemory/robots/DashboardRobot.kt deleted file mode 100644 index a2aefca..0000000 --- a/app/src/androidTest/java/io/github/gelassen/wordinmemory/robots/DashboardRobot.kt +++ /dev/null @@ -1,266 +0,0 @@ -package io.github.gelassen.wordinmemory.robots - -import android.view.KeyEvent -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.assertion.ViewAssertions.doesNotExist -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.RecyclerViewActions -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withId -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.matchers.CustomMatchers -import io.github.gelassen.wordinmemory.robots.Utils.atPositionByTitle -import io.github.gelassen.wordinmemory.robots.Utils.childAtPosition -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardAdapter -import org.hamcrest.CoreMatchers.allOf -import org.hamcrest.CoreMatchers.not -import org.hamcrest.Matchers -import org.hamcrest.core.StringContains - -class DashboardRobot { - - fun seesNoContent() : DashboardRobot { - seesListItems(resId = R.id.dashboardList, count = 0) - return this - } - - fun seesToolbar() : DashboardRobot { - onView(withId(R.id.toolbar)) - .check(matches(isDisplayed())) - return this - } - - fun seesFloatingActionButton() : DashboardRobot { - onView(withId(R.id.dashboardAddNewWord)) - .check(matches(isDisplayed())) - return this - } - - fun seesNoContentPlaceholder() : DashboardRobot { - onView(withId(R.id.noContentPlaceholder)) - .check(matches(isDisplayed())) - return this - } - - fun seesListItems(resId: Int, count: Int): DashboardRobot { - onView(withId(resId)) - .check(matches(isDisplayed())) - .check(matches(CustomMatchers().recyclerViewSizeMatch(count))) - return this - } - - fun seesAddNewItemDialog(): DashboardRobot { - onView(withId(R.id.save)) - .check(matches(isDisplayed())) - onView(withId(R.id.toTranslateEditText)) - .check(matches(isDisplayed())) - onView(withId(R.id.translateEditText)) - .check(matches(isDisplayed())) - return this - } - - fun seesListItemWithText(order: Int, text: String): DashboardRobot { - onView(withId(R.id.dashboardList)) - .check(matches(isDisplayed())) - onView(withId(R.id.dashboardList)) - .check(matches(atPositionByTitle(order, - ViewMatchers.withText(StringContains.containsString(text)) - ))) - return this - } - - fun doesNotSeeNoContentPlaceholder(): DashboardRobot { - onView(withId(R.id.noContentPlaceholder)) - .check(matches(not(isDisplayed()))) - return this - } - - fun doesNotSeeAddNewItemDialog(): DashboardRobot { - onView(withId(R.id.save)) - .check(doesNotExist()) - onView(withId(R.id.toTranslateEditText)) - .check(doesNotExist()) - onView(withId(R.id.translateEditText)) - .check(doesNotExist()) - return this - } - - fun scrollToTheFirst(): DashboardRobot { - onView(withId(R.id.dashboardList)) - .perform(RecyclerViewActions.scrollToPosition(0)) - return this - } - - fun scrollToTheLatest(): DashboardRobot { - onView(withId(R.id.dashboardList)) - .perform(RecyclerViewActions.scrollToLastPosition()) - return this - } - - fun seesTranslationOfSubject(textWithTranslation: String): DashboardRobot { - onView(withId(R.id.dashboardList)) - .check( - matches( - atPositionByTitle( - 0, - ViewMatchers.withText(StringContains.containsString(textWithTranslation)) - ) - ) - ) - return this - } - - fun doesNotSeeToolbar(): DashboardRobot { - onView(withId(R.id.toolbar)) - .check(matches(not(isDisplayed()))) - return this - } - - fun doesNotSeeFloatingActionButton(): DashboardRobot { - onView(withId(R.id.dashboardAddNewWord)) - .check(matches(not(isDisplayed()))) - return this - } - - fun seesSpecificNumbersOfItemsInList(count: Int): DashboardRobot{ - onView(withId(R.id.dashboardList)) - .check(Utils.assertItemCountInList(count)) - return this - } - - /* actions*/ - - fun clickOnFabButton(): DashboardRobot { - onView(withId(R.id.dashboardAddNewWord)) - .perform(ViewActions.click()) - return this - } - - fun enterNewWord(txtSrc: String, txtTranslation: String): DashboardRobot { - onView(withId(R.id.toTranslateEditText)) - .perform(ViewActions.replaceText(txtSrc)) - .perform(ViewActions.pressKey(KeyEvent.KEYCODE_ENTER)) - /*.perform(ViewActions.typeText(txtSrc)) // doesn't work due known issue of keyboard */ - onView(withId(R.id.translateEditText)) - .perform(ViewActions.replaceText(txtTranslation)) - .perform(ViewActions.pressKey(KeyEvent.KEYCODE_ENTER)) - /*.perform(ViewActions.typeText(txtTranslation)) // doesn't work due known issue of keyboard */ - return this - } - - fun saveNewWord(): DashboardRobot { - onView(withId(R.id.save)) - .perform(ViewActions.click()) - return this - } - - fun clickOnSubjectToStudy(position: Int, text: String): DashboardRobot { - onView(withId(R.id.dashboardList)) - .perform( - RecyclerViewActions.actionOnItemAtPosition( - position, - ViewActions.click() - ) - ) - return this - } - - fun markSubjectAsCompleted(position: Int, text: String = ""): DashboardRobot { - onView(withId(R.id.dashboardList)) - .perform( - RecyclerViewActions.actionOnItemAtPosition( - position, - Utils.CompleteAction() - ) - ) - return this - } - - fun clickMenuShowAll(): DashboardRobot { - onView( - Matchers.allOf( - ViewMatchers.withContentDescription("More options"), - ViewMatchers.withParent(ViewMatchers.withParent(withId(R.id.toolbar))), - isDisplayed() - ) - ) - .check(matches(isDisplayed())) - - onView( - Matchers.allOf( - ViewMatchers.withContentDescription("More options"), - childAtPosition( - childAtPosition( - withId(R.id.toolbar), - 1 - ), - 0 - ), - isDisplayed() - ) - ) - .perform(ViewActions.click()) - - onView( - allOf( - withId(androidx.appcompat.R.id.title), ViewMatchers.withText("Show all"), - childAtPosition( - childAtPosition( - withId(androidx.appcompat.R.id.content), - 0 - ), - 0 - ), - isDisplayed() - ) - ) - .perform(ViewActions.click()) - - return this - } - - fun clickMenuShowCompletedOnly(): DashboardRobot { - onView( - Matchers.allOf( - ViewMatchers.withContentDescription("More options"), - ViewMatchers.withParent(ViewMatchers.withParent(withId(R.id.toolbar))), - isDisplayed() - ) - ) - .check(matches(isDisplayed())) - - onView( - Matchers.allOf( - ViewMatchers.withContentDescription("More options"), - childAtPosition( - childAtPosition( - withId(R.id.toolbar), - 1 - ), - 0 - ), - isDisplayed() - ) - ) - .perform(ViewActions.click()) - - onView( - allOf( - withId(androidx.appcompat.R.id.title), ViewMatchers.withText("Show only not completed"), - childAtPosition( - childAtPosition( - withId(androidx.appcompat.R.id.content), - 0 - ), - 0 - ), - isDisplayed() - ) - ) - .perform(ViewActions.click()) - - return this - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/io/github/gelassen/wordinmemory/robots/Utils.kt b/app/src/androidTest/java/io/github/gelassen/wordinmemory/robots/Utils.kt deleted file mode 100644 index 72f5695..0000000 --- a/app/src/androidTest/java/io/github/gelassen/wordinmemory/robots/Utils.kt +++ /dev/null @@ -1,91 +0,0 @@ -package io.github.gelassen.wordinmemory.robots - -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import androidx.recyclerview.widget.RecyclerView -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction -import androidx.test.espresso.ViewAssertion -import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.matcher.BoundedMatcher -import androidx.test.espresso.matcher.ViewMatchers -import androidx.test.espresso.matcher.ViewMatchers.assertThat -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardAdapter -import org.hamcrest.Description -import org.hamcrest.Matcher -import org.hamcrest.Matchers -import org.hamcrest.TypeSafeMatcher - -object Utils { - - fun atPositionByTitle(position: Int, itemMatcher: Matcher): Matcher? { - checkNotNull(itemMatcher) - return object : BoundedMatcher(RecyclerView::class.java) { - override fun describeTo(description: Description) { - description.appendText("has item at position $position: ") - itemMatcher.describeTo(description) - } - - override fun matchesSafely(view: RecyclerView): Boolean { - val viewHolder = view.findViewHolderForAdapterPosition(position) - ?: // has no item on such position - return false - val item = matchByTitle(viewHolder.itemView) - return itemMatcher.matches(item) - } - } - } - - fun matchByTitle(root: View): View { - return root.findViewById(R.id.toTranslate) - } - - fun assertItemCountInList(count : Int): ViewAssertion { - return ViewAssertion { view, noViewFoundException -> - val adapter = (view as RecyclerView).adapter as DashboardAdapter - assertThat(adapter.itemCount, Matchers.`is`(count)) } - } - - class CompleteAction() : ViewAction { - override fun getDescription(): String { - return ("CompleteAction is performed") - } - - override fun getConstraints(): Matcher { - return Matchers.allOf( - ViewMatchers.isAssignableFrom( - RecyclerView::class.java - ), ViewMatchers.isDisplayed() - ) - } - - override fun perform(uiController: UiController?, view: View?) { - uiController?.loopMainThreadUntilIdle() - - ViewActions.click().perform( - uiController, - view?.findViewById(R.id.completeIcon) - ) - - uiController?.loopMainThreadUntilIdle() - } - } - - fun childAtPosition(parentMatcher: Matcher, position: Int): Matcher { - return object : TypeSafeMatcher() { - override fun describeTo(description: Description) { - description.appendText("Child at position $position in parent ") - parentMatcher.describeTo(description) - } - - override fun matchesSafely(view: View): Boolean { - val parent = view.parent - return (parent is ViewGroup && parentMatcher.matches(parent) - && view == parent.getChildAt(position)) - } - - } - } -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100644 index deaa423..0000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png deleted file mode 100644 index 73e7af4..0000000 Binary files a/app/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/App.kt b/app/src/main/java/io/github/gelassen/wordinmemory/App.kt deleted file mode 100644 index 637b784..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/App.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.gelassen.wordinmemory - -class App { - - companion object { - const val DATABASE_NAME = "WordInMemory.db" - const val TAG = "TAG" - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/AppApplication.kt b/app/src/main/java/io/github/gelassen/wordinmemory/AppApplication.kt deleted file mode 100644 index b65d135..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/AppApplication.kt +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.gelassen.wordinmemory - -import android.app.Application -import android.os.StrictMode -import android.os.StrictMode.ThreadPolicy -import androidx.work.Configuration -import io.github.gelassen.wordinmemory.backgroundjobs.MyWorkerFactory -import io.github.gelassen.wordinmemory.di.AppComponent -import io.github.gelassen.wordinmemory.di.AppModule -import io.github.gelassen.wordinmemory.di.DaggerAppComponent -import javax.inject.Inject - - -class AppApplication: Application(), Configuration.Provider { - - protected lateinit var diComponent: AppComponent - - @Inject - lateinit var myWorkerFactory: MyWorkerFactory - - override fun onCreate() { - super.onCreate() - - if (BuildConfig.DEBUG) { - turnOnNetworkStrictPolicy() - } - - diComponent = DaggerAppComponent - .builder() - .appModule(AppModule(this)) - .build() - diComponent.inject(this) - } - - fun getComponent(): AppComponent { - return diComponent - } - - override fun getWorkManagerConfiguration(): Configuration { - return Configuration.Builder() - .setMinimumLoggingLevel(android.util.Log.INFO) - .setWorkerFactory(myWorkerFactory) - .build() - } - - private fun turnOnNetworkStrictPolicy() { - StrictMode.setThreadPolicy( - ThreadPolicy.Builder() - .detectDiskReads() - .detectDiskWrites() - .detectNetwork() // or .detectAll() for all detectable problems - .penaltyLog() -// .penaltyDeath() - .build() - ) - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/AddNewRecordWorker.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/AddNewRecordWorker.kt deleted file mode 100644 index 68f821d..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/AddNewRecordWorker.kt +++ /dev/null @@ -1,309 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs - -import android.content.Context -import android.util.Log -import androidx.lifecycle.LifecycleOwner -import androidx.work.Data -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.BuildConfig -import io.github.gelassen.wordinmemory.backgroundjobs.AddNewRecordWorker.Companion.NON_INITIALISED -import io.github.gelassen.wordinmemory.backgroundjobs.pipline.IPipelineTask -import io.github.gelassen.wordinmemory.ml.PlainTranslator -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.network.Response -import io.github.gelassen.wordinmemory.repository.NetworkRepository -import io.github.gelassen.wordinmemory.repository.StorageRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import name.pilgr.pipinyin.PiPinyin -import java.lang.Exception -import java.lang.IllegalStateException -import java.util.Queue -import java.util.concurrent.ConcurrentLinkedQueue -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger - -data class Model( - var dataByWords: Queue = ConcurrentLinkedQueue(), - var dataWithTranslation: Queue> = ConcurrentLinkedQueue(), - var dataset: MutableList> = mutableListOf(), - val counter: AtomicInteger = AtomicInteger(NON_INITIALISED) -) -class AddNewRecordWorker( - val context: Context, - val params: WorkerParameters, - val translator: PlainTranslator, - val networkRepository: NetworkRepository, - val storageRepository: StorageRepository, - val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO -) : BaseWorker(context, params) { - - object Builder { - - const val EXTRA_TO_TRANSLATE_RECORD = "EXTRA_TO_TRANSLATE_RECORD" - - fun build(record: String): Data { - return workDataOf(EXTRA_TO_TRANSLATE_RECORD to record) - } - } - - companion object { - const val EXTRA_OPERATIONS_COUNT = 2 - const val NON_INITIALISED = -1 - } - - private val piPinyin = PiPinyin(context) - private val model = Model() - private var result = Result.failure() - - private fun disposeResources() { - piPinyin.recycle() - translator.close() - } - - override suspend fun doWork(): Result { - val record = inputData.getString(Builder.EXTRA_TO_TRANSLATE_RECORD)!! - val pipeline = mutableListOf( /* the order of tasks in the pipeline is matter */ - WordSegmentationTask(record), - TranslateTask(), - AddPinyinTask(), - PostProcessDataTask(record), - StorageTask() - ) - withContext(backgroundDispatcher) { - for (task in pipeline) { - task.process() - } - } - disposeResources() - return result - } - - private fun initiateCounter() { - model.counter.set(model.dataByWords.size + EXTRA_OPERATIONS_COUNT) - Log.d(App.TAG, "Counter is initiated ${model.counter}") - } - - private fun thereIsStillWork(): Boolean { - return model.counter.get() != 0 - } - - /** - * First task does not require extra checks, so there is no similar method. However it is - * good to preserve order in names to avoid cognitive mess with terms. - * */ - private fun isTaskTwoFinished(): Boolean { - return model.counter.get() != EXTRA_OPERATIONS_COUNT - } - - private fun isTaskThreeFinished(): Boolean { - return model.counter.get() != EXTRA_OPERATIONS_COUNT.minus(1) - } - - private fun debugCounterPrintln() { - Log.d(App.TAG, "Counter number ${model.counter.get()}") - } - - private inner class WordSegmentationTask(val record: String) : IPipelineTask { - - override suspend fun process(): IPipelineTask { - Log.d(App.TAG, "[part 1] addNewRecord::splitSentenceIntoWords") - val isFinished = AtomicBoolean(false) - while (thereIsStillWork() && !isFinished.get()) { - Log.d(App.TAG, "splitSentenceIntoWords inner loop. translator.isTranslationModelReady() ${translator.isTranslationModelReady()}") - Thread.sleep(1000) - // translation model will be required on the next step, but it would be better to wait it readiness here - if (translator.isTranslationModelReady()) { - isFinished.set(true) - val response = networkRepository.splitChineseSentenceIntoWords(record) - when (response) { - is Response.Data -> { processResponse(response) } - is Response.Error -> { processErrorResponse(response) } - } - } else { - continue - } - } - return this - } - - private fun processResponse(response: Response.Data>>) { - if (isNotValidResponse(response)) { - val errorMsg = "Received data from backend either empty or has more than one record" - val outputData = workDataOf(Consts.KEY_ERROR_MSG to errorMsg) - result = Result.failure(outputData) - } else { - val data = ConcurrentLinkedQueue()//mutableListOf>() - for (item in response.data.get(0)) { - data.add(item) // sentence is split to words, but not translated yet; live translation as empty string "" - } - model.dataByWords = data - addWholeRecordAsExtraWord() - initiateCounter() - } - } - - /** - * In case of sentence in record, it would be good to have in vocabulary to. The best way so - * far is to add it into dataset at the beginning of the pipeline - * */ - private fun addWholeRecordAsExtraWord() { - model.dataByWords.add(record) - } - - private fun isNotValidResponse(response: Response.Data>>): Boolean { - return response.data.isEmpty() - || response.data.get(0).isEmpty() - || response.data.size > 1 - } - - private fun processErrorResponse(response: Response.Error) { - val errorMsg = when (response) { - is Response.Error.Message -> { response.msg } - is Response.Error.Exception -> { "Failed to classify text with error" } - } - val outputData = workDataOf(Consts.KEY_ERROR_MSG to errorMsg) - result = Result.failure(outputData) - } - - } - - private inner class TranslateTask: IPipelineTask { - - override suspend fun process(): IPipelineTask { - while (isTaskTwoFinished()) { - Log.d(App.TAG, "processWordsInQueue inner loop") - Thread.sleep(1000) - if (model.dataByWords.isEmpty()) { - continue - } else { - translate(model.dataByWords.poll()!!) - } - } - return this - } - - private fun translate(word: String) { - Log.d(App.TAG, "[part 2] addNewRecord::translate") - debugCounterPrintln() - translator.translateChineseText(word, object: PlainTranslator.ITranslationListener { - - override fun onTranslationSuccess(translatedText: String) { - Log.d(App.TAG, "onTranslationSuccess $word and $translatedText") - model.dataWithTranslation.add(Pair(word, translatedText)) - model.counter.decrementAndGet() - debugCounterPrintln() - } - - override fun onTranslationFailed(exception: Exception) { - // it should never happen, at this product version there is no right handler for it - Log.e(App.TAG, "onTranslationFailed for word $word", exception) - } - - override fun onModelDownloaded() { - TODO("Not yet implemented") - } - - override fun onModelDownloadFail(exception: Exception) { - TODO("Not yet implemented") - } - - }) - } - - } - - private inner class AddPinyinTask: IPipelineTask { - - override suspend fun process(): IPipelineTask { - Log.d(App.TAG, "[part 3] addNewRecord::extendWithPinyin") - debugCounterPrintln() - while (isTaskThreeFinished()) { - Thread.sleep(1000) - if (isTaskTwoFinished()) { - continue - } else { - debugCounterPrintln() - Log.d(App.TAG, "model.dataWithTranslation ${model.dataWithTranslation}") - model.dataset = model.dataWithTranslation.map { it -> - val pinyin = piPinyin.toPinyin(it.first, " ") - Pair("%s / %s".format(it.first, pinyin), it.second) - }.toMutableList() - Log.d(App.TAG, "Extend translation with pinyin ${model.dataset}") - model.counter.decrementAndGet() - } - } - return this - } - - } - - private inner class PostProcessDataTask(val record: String): IPipelineTask { - - private var forbiddenSymbols: List = mutableListOf("", " ", ",", ", ") - - override suspend fun process(): IPipelineTask { - cleanup() - return this - } - - private fun cleanup() { - model.dataset = model.dataset.filter { it -> !forbiddenSymbols.contains(it.second) }.toMutableList() - } - - @Deprecated("An origin record has been added into dataset at the beginning of the " + - "pipeline. This method has been obsolete.") - private fun addSentenceAsWhole() { - val originSentence = record - var originSentencePinyin = "" - var originSentenceTranslation = "" - for (item in model.dataset) { - val originAndPinyin = item.first.trim().split("/") - if (originAndPinyin.size != 2) { - val errorMsg = "PostProcessDataTask() find not expected origin hanzi&pinyin pair: ${item.first} / ${item.second}" - if (BuildConfig.DEBUG) { - throw IllegalStateException(errorMsg) - } else { - Log.e(App.TAG, errorMsg) - } - } else { - originSentencePinyin = originSentencePinyin.plus(originAndPinyin.last()) - } - originSentenceTranslation = originSentenceTranslation.plus(item.second).plus(" ") - } - model.dataset.add( - Pair( - "%s / %s".format(originSentence, originSentencePinyin), - originSentenceTranslation) - ) - } - - } - - private inner class StorageTask: IPipelineTask { - override suspend fun process(): IPipelineTask { - Log.d(App.TAG, "[part 4] addNewRecord::save") - debugCounterPrintln() - while (thereIsStillWork()) { - Thread.sleep(1000) - if (isTaskThreeFinished()) { - continue - } else { - val toDomainObjects = model.dataset.map { it -> SubjectToStudy(toTranslate = it.first, translation = it.second) } - Log.d(App.TAG, "Data to save ${toDomainObjects}") - withContext(backgroundDispatcher) { - storageRepository.saveSubject(*toDomainObjects.map { it }.toTypedArray()) - } - result = Result.success() - model.counter.decrementAndGet() - } - - } - return this - } - - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/BackupVocabularyWorker.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/BackupVocabularyWorker.kt deleted file mode 100644 index c8f3ef0..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/BackupVocabularyWorker.kt +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs - -import android.content.Context -import android.net.Uri -import android.util.Log -import androidx.work.Data -import androidx.work.WorkerParameters -import androidx.work.workDataOf -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.backgroundjobs.BaseWorker.Consts.KEY_ERROR_MSG -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.model.convertToJson -import io.github.gelassen.wordinmemory.repository.StorageRepository -import io.github.gelassen.wordinmemory.utils.FileUtils -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.json.JSONArray -import org.json.JSONObject -import java.io.BufferedWriter -import java.io.IOException -import java.io.OutputStreamWriter -import java.util.Objects - - -class BackupVocabularyWorker( - val context: Context, - val params: WorkerParameters, - val storageRepository: StorageRepository, - val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO) : BaseWorker(context, params) { - - object Builder { - - const val EXTRA_BACKUP_URI = "EXTRA_BACKUP_URI" - - fun build(uriToBackupDataFile: Uri): Data { - return workDataOf(EXTRA_BACKUP_URI to uriToBackupDataFile.toString()) - } - } - - override suspend fun doWork(): Result { - var result = Result.failure() - withContext(backgroundDispatcher) { - if (FileUtils().isExternalStorageAvailable()) { - val dataset = storageRepository.getSubjectsNonFlow() - result = writeDatasetToExternalFile(dataset) - } - } - return result - } - - private fun writeDatasetToExternalFile(dataset: List): Result { - var result: Result = Result.success() - try { - val destinationUri = Uri.parse(inputData.getString(RestoreVocabularyWorker.Builder.EXTRA_BACKUP_URI)) - writeAsJsonArray(dataset, destinationUri) - } catch (ex: Exception) { - val errorMsg = "Failed to backup database into external storage file" - Log.e(App.TAG, errorMsg, ex) - val outputData = workDataOf(KEY_ERROR_MSG to errorMsg) - result = Result.failure(outputData) - } - return result - } - - private fun writeAsJsonArray(dataset: List, destinationUri: Uri) { - Log.d(App.TAG, "Save file to a destination folder $destinationUri") - val jsonArray = JSONArray() - dataset.forEach { it -> jsonArray.put(JSONObject(it.convertToJson())) } - writeTextToUri(context, jsonArray.toString(), destinationUri) - } - - @Throws(IOException::class) - fun writeTextToUri(context: Context, dataset: String, uri: Uri?) { - context.contentResolver.openOutputStream(uri!!).use { outputStream -> - BufferedWriter( - OutputStreamWriter(Objects.requireNonNull(outputStream)) - ).use { writer -> - writer.write(dataset) - writer.flush() - writer.close() - } - } - } - - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/BaseWorker.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/BaseWorker.kt deleted file mode 100644 index b2fc3ac..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/BaseWorker.kt +++ /dev/null @@ -1,94 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs - -import android.app.Notification -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.Context -import android.graphics.Color -import android.os.Build -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat -import androidx.work.CoroutineWorker -import androidx.work.ForegroundInfo -import androidx.work.WorkManager -import androidx.work.WorkerParameters -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers - -open abstract class BaseWorker -constructor( - context: Context, - params: WorkerParameters, - backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO -) : CoroutineWorker(context, params) { - - object Consts { - const val KEY_ERROR_MSG = "KEY_ERROR_MSG" - } - - protected var foregroundIndo: ForegroundInfo - - - private val notificationId = 1001 - private val notificationChannelId = 10001 - private val notificationChannelName = "WordsInMemory background task is working" - - private val notificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as - NotificationManager - init { - val progress = "Start execute tx on the chain" - foregroundIndo = createForegroundInfo(progress) - } - - override suspend fun doWork(): Result { - TODO("Not yet implemented") - } - - override suspend fun getForegroundInfo(): ForegroundInfo { - return foregroundIndo - } - - private fun createForegroundInfo(progress: String): ForegroundInfo { - //val id = "10001"//applicationContext.getString(R.string.notification_channel_id) - val title = "Title"//applicationContext.getString(R.string.notification_title) - val cancel = "Cancel"//applicationContext.getString(R.string.cancel_download) - // This PendingIntent can be used to cancel the worker - val intent = WorkManager.getInstance(applicationContext) - .createCancelPendingIntent(getId()) - - // Create a Notification channel if necessary - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createChannel() - } - - val notification = NotificationCompat.Builder(applicationContext, notificationChannelId.toString()) - .setContentTitle(title) - .setTicker(title) - .setContentText(progress) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setSmallIcon(dagger.android.support.R.drawable.abc_ic_arrow_drop_right_black_24dp) - .setOngoing(true) - // Add the cancel action to the notification which can - // be used to cancel the worker - .addAction(android.R.drawable.ic_delete, cancel, intent) - .build() - - return ForegroundInfo(notificationId, notification) // TODO shall we add FOREGROUND_TYPE field here? - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun createChannel(): Int { - // Create a Notification channel - val chan = NotificationChannel( - notificationChannelId.toString(), - notificationChannelName, - NotificationManager.IMPORTANCE_DEFAULT - ) - chan.lightColor = Color.BLUE - chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE - notificationManager.createNotificationChannel(chan) - return notificationChannelId - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/Extensions.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/Extensions.kt deleted file mode 100644 index e62aea2..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/Extensions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs - -import androidx.work.* - -inline fun WorkManager.getWorkRequest(inputData: Data): OneTimeWorkRequest { - return OneTimeWorkRequestBuilder() - .setConstraints( - Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - ) - .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) - .setInputData(inputData) /* input data for worker */ - .build() -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/MyWorkerFactory.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/MyWorkerFactory.kt deleted file mode 100644 index 825de13..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/MyWorkerFactory.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs - -import android.content.Context -import androidx.work.ListenableWorker -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters -import io.github.gelassen.wordinmemory.ml.PlainTranslator -import io.github.gelassen.wordinmemory.repository.NetworkRepository -import io.github.gelassen.wordinmemory.repository.StorageRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import javax.inject.Inject - -class MyWorkerFactory @Inject constructor( - val translator: PlainTranslator, - val storageRepository: StorageRepository, - val networkRepository: NetworkRepository, - val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO -): WorkerFactory() { - override fun createWorker( - appContext: Context, - workerClassName: String, - workerParameters: WorkerParameters - ): ListenableWorker? { - return when(workerClassName) { - BackupVocabularyWorker::class.java.name -> { - BackupVocabularyWorker( - context = appContext, - params = workerParameters, - storageRepository = storageRepository, - backgroundDispatcher = backgroundDispatcher - ) - } - RestoreVocabularyWorker::class.java.name -> { - RestoreVocabularyWorker( - context = appContext, - params = workerParameters, - storageRepository = storageRepository, - backgroundDispatcher = backgroundDispatcher - ) - } - AddNewRecordWorker::class.java.name -> { - AddNewRecordWorker( - context = appContext, - params = workerParameters, - translator = translator, - networkRepository = networkRepository, - storageRepository = storageRepository, - backgroundDispatcher = backgroundDispatcher - ) - } - else -> throw IllegalStateException("Unknown worker. Did you forget to register a new type of worker in the factory?") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/RestoreVocabularyWorker.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/RestoreVocabularyWorker.kt deleted file mode 100644 index 9b8624a..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/RestoreVocabularyWorker.kt +++ /dev/null @@ -1,116 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs - -import android.content.Context -import android.net.Uri -import android.util.Log -import androidx.documentfile.provider.DocumentFile -import androidx.work.Data -import androidx.work.WorkerParameters -import androidx.work.hasKeyWithValueOfType -import androidx.work.workDataOf -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.backgroundjobs.BaseWorker.Consts.KEY_ERROR_MSG -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.model.fromJson -import io.github.gelassen.wordinmemory.repository.StorageRepository -import io.github.gelassen.wordinmemory.utils.FileUtils -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import org.json.JSONArray -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.util.Objects - - -class RestoreVocabularyWorker( - val context: Context, - val params: WorkerParameters, - val storageRepository: StorageRepository, - val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO -) : BaseWorker(context, params) { - - object Builder { - - const val EXTRA_BACKUP_URI = "EXTRA_BACKUP_URI" - - fun build(backupUri: Uri): Data { - return workDataOf(EXTRA_BACKUP_URI to backupUri.toString()) - } - } - override suspend fun doWork(): Result { - var result = Result.failure() - try { - if (!FileUtils().isExternalStorageAvailable()) { - val errorMsg = "External storage is not available" - result = prepareFailureResult(errorMsg) - } else if (!inputData.hasKeyWithValueOfType(Builder.EXTRA_BACKUP_URI)) { - val errorMsg = "There is no uri for backup data. Did you forget to pass it when had prepared Worker?" - result = prepareFailureResult(errorMsg) - } else if (!DocumentFile.fromSingleUri( - context, - Uri.parse(inputData.getString(Builder.EXTRA_BACKUP_URI)) - )!!.exists() - ) { - val errorMsg = "Uri is valid, but file doesn't exist anymore. Did you or any your apps unexpectedly had removed it?" - result = prepareFailureResult(errorMsg) - } else { - val backupUri = Uri.parse(inputData.getString(Builder.EXTRA_BACKUP_URI)) - val dataset = getDataFromBackup(backupUri) - // TODO reset uid to zero to create new ones - it will allow to integrated existing backup with recently added rows - storageRepository.saveSubject(*dataset.map { it }.toTypedArray()) - result = Result.success() - } - } catch (ex: Exception) { - val errorMsg = "Failed to restore a vocabulary" - Log.e(App.TAG, errorMsg, ex) - result = prepareFailureResult(errorMsg) - } - return result - } - - private fun prepareFailureResult(errorMsg: String): Result { - val outputData = workDataOf(KEY_ERROR_MSG to errorMsg) - return Result.failure(outputData) - } - - private fun getDataFromBackup(backupUri: Uri): MutableList { - val result = mutableListOf() - Log.d(App.TAG, "Plain read from uri $backupUri") - val jsonArrayAsString = readTextFromUri(context, backupUri) - result.addAll(getDatasetFromText(jsonArrayAsString)) - return result - } - - private fun getDatasetFromText(jsonArrayAsString: String): MutableList { - val result = mutableListOf() - val jsonArray = JSONArray(jsonArrayAsString) - Log.d(App.TAG, "JSON array: ${jsonArray.toString()}") - for (idx in 0 until jsonArray.length()) { - Log.d(App.TAG, "Json item at index ${idx} is ${jsonArray.getJSONObject(idx).toString()}") - jsonArray.getJSONObject(idx) - result.add( - SubjectToStudy().fromJson(jsonArray.optJSONObject(idx).toString()) - ) - } - return result - } - - @Throws(IOException::class) - fun readTextFromUri(context: Context, uri: Uri?): String { - val stringBuilder = StringBuilder() - context.contentResolver.openInputStream(uri!!).use { inputStream -> - BufferedReader( - InputStreamReader(Objects.requireNonNull(inputStream)) - ).use { reader -> - var line: String? - while (reader.readLine().also { line = it } != null) { - stringBuilder.append(line) - } - } - inputStream!!.close() - } - return stringBuilder.toString() - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/pipline/IPipeline.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/pipline/IPipeline.kt deleted file mode 100644 index d577732..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/pipline/IPipeline.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs.pipline - -import java.util.LinkedList -import java.util.Queue -import java.util.concurrent.ConcurrentLinkedQueue - -interface IPipeline { - - val list: Queue - - fun run(): IPipeline -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/pipline/IPipelineTask.kt b/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/pipline/IPipelineTask.kt deleted file mode 100644 index e6c65f9..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/backgroundjobs/pipline/IPipelineTask.kt +++ /dev/null @@ -1,9 +0,0 @@ -package io.github.gelassen.wordinmemory.backgroundjobs.pipline - -interface IPipelineTask { - - /** - * The origin intent is to use it inside coroutine worker, so it likely will operate with coroutine - * */ - suspend fun process(): IPipelineTask -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/customviews/GroupChoiceView.kt b/app/src/main/java/io/github/gelassen/wordinmemory/customviews/GroupChoiceView.kt deleted file mode 100644 index 3f32b64..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/customviews/GroupChoiceView.kt +++ /dev/null @@ -1,129 +0,0 @@ -package io.github.gelassen.wordinmemory - -import android.content.Context -import android.graphics.Canvas -import android.util.AttributeSet -import android.util.TypedValue -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.core.view.setPadding -import io.github.gelassen.wordinmemory.R - -class GroupChoiceView : LinearLayout { - - private var isOfferChosen = true - private var enabledTextColor: Int = -1 - private var disabledTextColor: Int = -1 - - constructor(context: Context?) : super(context) { } - constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init(context!!, attrs) } - constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context!!, attrs) - } - constructor( - context: Context?, - attrs: AttributeSet?, - defStyleAttr: Int, - defStyleRes: Int - ) : super(context, attrs, defStyleAttr, defStyleRes) { - init(context!!, attrs) - } - - override fun addView(child: View) { - super.addView(child) - } - - override fun addView(child: View, index: Int) { - throw IllegalAccessException("Method is disabled. Component contains only two items which is added at launch") - } - - override fun addView(child: View, width: Int, height: Int) { - throw IllegalAccessException("Method is disabled. Component contains only two items which is added at launch") - } - - override fun addView(child: View, params: ViewGroup.LayoutParams) { - super.addView(child, params) - } - - override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) { - super.addView(child, index, params) - } - - override fun onDraw(canvas: Canvas?) { - super.onDraw(canvas) - } - - fun isOfferSelected() : Boolean { - return isOfferChosen - } - - fun onOfferClick() { - isOfferChosen = true - updateColors() - } - - fun onDemandClick() { - isOfferChosen = false - updateColors() - } - - fun updateColors() { - findViewById(R.id.offer_option).setTextColor(if (isOfferChosen) enabledTextColor else disabledTextColor) - findViewById(R.id.demand_option).setTextColor(if (isOfferChosen) disabledTextColor else enabledTextColor) - } - - private fun init(context: Context, attrs: AttributeSet?) { -/* enabledTextColor = ContextCompat.getColor(context, R.color.blue_light) - disabledTextColor = ContextCompat.getColor(context, R.color.grey) - - addView( - getView(text = resources.getString(R.string.offers_title), R.id.offer_option), - getViewLayoutParams( - marginStart = context.resources.getDimensionPixelOffset(R.dimen.base_margin), - marginEnd = 0 - )) - addView( - getView(text = resources.getString(R.string.demands_title), R.id.demand_option), - getViewLayoutParams( - marginStart = 0, - marginEnd = context.resources.getDimensionPixelOffset(R.dimen.base_margin) - ) - ) - findViewById(R.id.offer_option).setTextColor(if (isOfferChosen) enabledTextColor else disabledTextColor) - findViewById(R.id.demand_option).setTextColor(if (isOfferChosen) disabledTextColor else enabledTextColor)*/ - } - - private fun getView(text: String, id: Int) : TextView { - val view = TextView(context) - view.setId(id) - view.text = text - view.setPadding(context.resources.getDimensionPixelOffset(R.dimen.selectable_view_padding)) - view.setTextColor(context.resources.getColor(R.color.blue_light)) - view.isClickable = true - view.isFocusable = true - view.gravity = Gravity.CENTER - val outValue = TypedValue() - getContext().theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true) - view.setBackgroundResource(outValue.resourceId) - return view - } - - private fun getViewLayoutParams(marginStart: Int, marginEnd: Int) : LinearLayout.LayoutParams { - val params = LayoutParams( - LayoutParams.WRAP_CONTENT, - LayoutParams.WRAP_CONTENT - ) - params.marginStart = marginStart - params.marginEnd = marginEnd - return params - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/customviews/TwoStateTextView.kt b/app/src/main/java/io/github/gelassen/wordinmemory/customviews/TwoStateTextView.kt deleted file mode 100644 index a964f10..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/customviews/TwoStateTextView.kt +++ /dev/null @@ -1,61 +0,0 @@ -package io.github.gelassen.wordinmemory - -import android.content.Context -import android.content.res.TypedArray -import android.graphics.Canvas -import android.os.Build -import android.util.AttributeSet -import androidx.annotation.RequiresApi -import androidx.appcompat.widget.AppCompatTextView - - -class TwoStateTextView : AppCompatTextView { - - private var isChosen = true - private var isEnabledTextColor: Int = -1 - private var isDisabledTextColor: Int = -1 - - constructor(context: Context) : super(context) {} - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init(context, attrs) - } - @RequiresApi(Build.VERSION_CODES.M) - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context, attrs) -/* // TODO replace on color state list when minSdk will be API 23 or higher - val colorStateList = context.getColorStateList(R.color.selector_two_state_view)*/ - isEnabledTextColor = context.getColor(R.color.blue_light) - isDisabledTextColor = context.getColor(R.color.grey) - } - - @RequiresApi(Build.VERSION_CODES.M) - override fun onDraw(canvas: Canvas?) { - setTextColor( - if (isChosen) context.getColor(R.color.blue_light) - else context.getColor(R.color.grey) - ) - super.onDraw(canvas) - } - - override fun callOnClick(): Boolean { - isChosen = !isChosen - invalidateOutline() - return true - } - - private fun init(context: Context, attrs: AttributeSet?) { - val attributes: TypedArray = context.obtainStyledAttributes( - attrs, - R.styleable.TwoStateTextView - ) - isChosen = attributes.getBoolean( - R.styleable.TwoStateTextView_selected, - true - ) - attributes.recycle() - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/di/AppComponent.kt b/app/src/main/java/io/github/gelassen/wordinmemory/di/AppComponent.kt deleted file mode 100644 index a0b36c0..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/di/AppComponent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.gelassen.wordinmemory.di - -import dagger.Component -import io.github.gelassen.wordinmemory.AppApplication -import io.github.gelassen.wordinmemory.dialogs.AddItemBottomSheetDialogFragment -import io.github.gelassen.wordinmemory.dialogs.AddItemDialogFragment -import io.github.gelassen.wordinmemory.ui.addnewrecord.AddNewRecordFragment -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardFragment -import javax.inject.Singleton - -@Singleton -@Component( - modules = [ - ViewModelModule::class, - AppModule::class - ] -) -interface AppComponent { - fun inject(subj: AddItemBottomSheetDialogFragment) - fun inject(subj: DashboardFragment) - fun inject(subj: AddItemDialogFragment) - fun inject(subj: AppApplication) - fun inject(subj: AddNewRecordFragment) -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/di/AppModule.kt b/app/src/main/java/io/github/gelassen/wordinmemory/di/AppModule.kt deleted file mode 100644 index 32c9062..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/di/AppModule.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.gelassen.wordinmemory.di - -import android.app.Application -import android.content.Context -import dagger.Module -import dagger.Provides -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.backgroundjobs.MyWorkerFactory -import io.github.gelassen.wordinmemory.di.AppModule.Consts.DISPATCHER_IO -import io.github.gelassen.wordinmemory.ml.PlainTranslator -import io.github.gelassen.wordinmemory.network.IApi -import io.github.gelassen.wordinmemory.repository.NetworkRepository -import io.github.gelassen.wordinmemory.repository.StorageRepository -import io.github.gelassen.wordinmemory.storage.AppDatabase -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import javax.inject.Named -import javax.inject.Singleton - -@Module -class AppModule(val application: Application) { - - object Consts { - const val DISPATCHER_IO = "DISPATCHER_IO" - } - - @Provides - fun providesStorageRepository(database: AppDatabase): StorageRepository { - return StorageRepository(database.subjectToStudyDao()) - } - - @Provides - @Singleton - fun providesDatabase(): AppDatabase { - return AppDatabase.getInstance(application) - } - - @Provides - fun provideApplication(): Application { - return application - } - - @Provides - fun provideContext(): Context { - return application - } - - @Singleton - @Provides - fun provideApi(): IApi { - val url = application.getString(R.string.endpoint) - val logging = HttpLoggingInterceptor() - logging.setLevel(HttpLoggingInterceptor.Level.BODY) - val httpClient = OkHttpClient - .Builder() - .addInterceptor(logging) - .build() - val retrofit = Retrofit.Builder() - .addConverterFactory(GsonConverterFactory.create()) - .client(httpClient) - .baseUrl(url) - .build() - - return retrofit.create(IApi::class.java) - } - - @Provides - fun provideNetworkRepository(api: IApi): NetworkRepository { - return NetworkRepository(api) - } - - @Provides - @Named(DISPATCHER_IO) - fun providesNetworkDispatcher(): CoroutineDispatcher { - return Dispatchers.IO - } - - @Provides - fun providesMyWorkerFactory( - translator: PlainTranslator, - storageRepository: StorageRepository, - networkRepository: NetworkRepository, - @Named(DISPATCHER_IO) dispatcher: CoroutineDispatcher - ): MyWorkerFactory { - return MyWorkerFactory( - translator = translator, - storageRepository = storageRepository, - networkRepository = networkRepository, - backgroundDispatcher = dispatcher - ) - } - - @Singleton - @Provides - fun provideTranslator(): PlainTranslator { - return PlainTranslator(null) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelFactory.java b/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelFactory.java deleted file mode 100644 index 52a69d4..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelFactory.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.github.gelassen.wordinmemory.di; - -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; - -import java.util.Map; - -import javax.inject.Inject; -import javax.inject.Provider; - -public class ViewModelFactory implements ViewModelProvider.Factory { - private final Map, Provider> viewModels; - - @Inject - public ViewModelFactory(Map, Provider> viewModels) { - this.viewModels = viewModels; - } - - @Override - public T create(Class modelClass) { - Provider viewModelProvider = viewModels.get(modelClass); - - if (viewModelProvider == null) { - throw new IllegalArgumentException("model class " + modelClass + " not found"); - } - - return (T) viewModelProvider.get(); - } -} - diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelKey.java b/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelKey.java deleted file mode 100644 index f81d7da..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelKey.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.gelassen.wordinmemory.di; - -import androidx.lifecycle.ViewModel; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import dagger.MapKey; - - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -@MapKey -public @interface ViewModelKey { - Class value(); -} diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelModule.java b/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelModule.java deleted file mode 100644 index c92a8f6..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/di/ViewModelModule.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.gelassen.wordinmemory.di; - -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; - -import javax.inject.Singleton; - -import dagger.Binds; -import dagger.Module; -import dagger.multibindings.IntoMap; -import io.github.gelassen.wordinmemory.ui.addnewrecord.NewRecordViewModel; -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardViewModel; - -@Module -public abstract class ViewModelModule { - - @Binds - abstract ViewModelProvider.Factory bindViewModelFactory(ViewModelFactory viewModelFactory); - - @Binds - @IntoMap - @ViewModelKey(DashboardViewModel.class) - @Singleton - abstract ViewModel dashboardViewModel(DashboardViewModel vm); - - @Binds - @IntoMap - @ViewModelKey(NewRecordViewModel.class) - @Singleton - abstract ViewModel newRecordViewModel(NewRecordViewModel vm); -} diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemBottomSheetDialogFragment.kt b/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemBottomSheetDialogFragment.kt deleted file mode 100644 index 4744af5..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemBottomSheetDialogFragment.kt +++ /dev/null @@ -1,118 +0,0 @@ -package io.github.gelassen.wordinmemory.dialogs - -import android.content.Context -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager -import android.widget.Toast -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.AppApplication -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.databinding.AddItemFragmentBinding -import io.github.gelassen.wordinmemory.di.ViewModelFactory -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.ui.addnewrecord.NewRecordViewModel -import javax.inject.Inject - -class AddItemBottomSheetDialogFragment: BottomSheetDialogFragment() { - - private lateinit var binding: AddItemFragmentBinding - - @Inject - lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: NewRecordViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setStyle(DialogFragment.STYLE_NORMAL, R.style.DialogStyle) - (requireActivity().application as AppApplication).getComponent().inject(this) - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // keep an eye on owner parameter, it should be the same scope for view model which is shared among component -// viewModel = ViewModelProvider(requireParentFragment(), viewModelFactory).get(DashboardViewModel::class.java) - viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(NewRecordViewModel::class.java) - binding = AddItemFragmentBinding.inflate(inflater, container, false) - binding.model = viewModel - binding.withBackend = isWithExperimentalFeatureSupport() - binding.isOnEdit = false - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - if (!requireArguments().isEmpty - && requireArguments().containsKey(EXTRA_DATA)) { - val data = requireArguments().getParcelable(EXTRA_DATA) - viewModel.wordToTranslate.set(data?.toTranslate) - viewModel.translation.set(data?.translation) - binding.isOnEdit = true - } - - lifecycleScope.launchWhenStarted { - viewModel.uiState.collect { - if (it.errors.isNotEmpty()) { - Toast.makeText(requireContext(), it.errors.first(), Toast.LENGTH_SHORT) - .show() - viewModel.removeError(it.errors.first()) - } - } - } - - binding.save.setOnClickListener { - Log.d(App.TAG, "${viewModel.wordToTranslate.get()}") - if (requireArguments().isEmpty) { - if (isWithExperimentalFeatureSupport()) { - viewModel.start() - } else { - viewModel.addItem() - } - } else { - viewModel.updateItem(requireArguments().getParcelable(EXTRA_DATA)!!) - } - dismiss() - } - } - - fun showSoftKeyboard(view: View) { - if (view.requestFocus()) { - val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) - } - } - - private fun isWithExperimentalFeatureSupport(): Boolean { - return resources.getBoolean(R.bool.with_backend) - } - - companion object { - - const val TAG = "AddItemBottomSheetDialogFragment" - const val EXTRA_DATA = "EXTRA_DATA" - - fun newInstance(args: Bundle): AddItemBottomSheetDialogFragment { - val fragment = AddItemBottomSheetDialogFragment() - fragment.arguments = args - return fragment - } - - fun newInstance(subject: SubjectToStudy): AddItemBottomSheetDialogFragment { - val fragment = AddItemBottomSheetDialogFragment() - val data = Bundle() - data.putParcelable(EXTRA_DATA, subject) - fragment.arguments = data - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemDialogFragment.kt b/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemDialogFragment.kt deleted file mode 100644 index f50d43c..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemDialogFragment.kt +++ /dev/null @@ -1,90 +0,0 @@ -package io.github.gelassen.wordinmemory.dialogs - -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.ViewModelProvider -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import io.github.gelassen.wordinmemory.AppApplication -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.databinding.AddItemFragmentBinding -import io.github.gelassen.wordinmemory.di.ViewModelFactory -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.ui.addnewrecord.NewRecordViewModel -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardViewModel -import javax.inject.Inject - -class AddItemDialogFragment: DialogFragment() { - - companion object { - - const val TAG = "AddItemDialogFragment" - const val EXTRA_DATA = "EXTRA_DATA" - - fun newInstance(args: Bundle): AddItemDialogFragment { - val fragment = AddItemDialogFragment() - fragment.arguments = args - return fragment - } - - fun newInstance(subject: SubjectToStudy): AddItemDialogFragment { - val fragment = AddItemDialogFragment() - val data = Bundle() - data.putParcelable(EXTRA_DATA, subject) - fragment.arguments = data - return fragment - } - } - - private lateinit var binding: AddItemFragmentBinding - - @Inject - lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: NewRecordViewModel - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - (requireActivity().application as AppApplication).getComponent().inject(this) - // keep an eye on owner parameter, it should be the same scope for view model which is shared among component - viewModel = ViewModelProvider(requireParentFragment(), viewModelFactory) - .get(NewRecordViewModel::class.java) - binding = AddItemFragmentBinding.inflate(LayoutInflater.from(context)) - binding.withBackend = isWithExperimentalFeatureSupport() - binding.model = viewModel - binding.title.visibility = View.VISIBLE - binding.isOnEdit = false - binding.save.setOnClickListener { - if (requireArguments().isEmpty) { - if (isWithExperimentalFeatureSupport()) { - viewModel.start() - } else { - viewModel.addItem() - } - } else { - viewModel.updateItem(requireArguments().getParcelable( - AddItemBottomSheetDialogFragment.EXTRA_DATA - )!!) - } - dismiss() - } - preSetIfNecessary() - return MaterialAlertDialogBuilder(requireContext()) - .setView(binding.root) - .create() - } - - private fun preSetIfNecessary() { - if (arguments?.containsKey(EXTRA_DATA) == true) { - val data = requireArguments().getParcelable(EXTRA_DATA)!! - viewModel.wordToTranslate.set(data.toTranslate) - viewModel.translation.set(data.translation) - binding.isOnEdit = true - } - } - - private fun isWithExperimentalFeatureSupport(): Boolean { - return resources.getBoolean(R.bool.with_backend) - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemDialogProxy.kt b/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemDialogProxy.kt deleted file mode 100644 index e64cdf3..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/dialogs/AddItemDialogProxy.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.gelassen.wordinmemory.dialogs - -import android.app.Activity -import android.os.Bundle -import androidx.fragment.app.FragmentManager -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.utils.ConfigParams - -class AddItemDialogProxy { - - private val configParams: ConfigParams = ConfigParams() - - fun show(selectedSubject: SubjectToStudy?, childFragmentManager: FragmentManager, activity: Activity) { - if (configParams.showDialogAsBottomSheet(activity)) { - showBottomSheetDialog(selectedSubject, childFragmentManager) - } else { - showDialogFragment(selectedSubject, childFragmentManager) - } - } - - private fun showDialogFragment(selectedSubject: SubjectToStudy?, childFragmentManager: FragmentManager) { - if (selectedSubject == null) { - AddItemDialogFragment.newInstance(Bundle.EMPTY) - .show(childFragmentManager, AddItemDialogFragment.TAG) - } else { - AddItemDialogFragment.newInstance(selectedSubject) - .show(childFragmentManager, AddItemDialogFragment.TAG) - } - } - - private fun showBottomSheetDialog(selectedSubject: SubjectToStudy?, childFragmentManager: FragmentManager) { - childFragmentManager.let { - if (selectedSubject == null) { - AddItemBottomSheetDialogFragment.newInstance(Bundle.EMPTY) - .show(it, AddItemBottomSheetDialogFragment.TAG) - } else { - AddItemBottomSheetDialogFragment.newInstance(selectedSubject) - .show(it, AddItemBottomSheetDialogFragment.TAG) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ml/OCRTranslator.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ml/OCRTranslator.kt deleted file mode 100644 index d6e479a..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ml/OCRTranslator.kt +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.gelassen.wordinmemory.ml - - -class OCRTranslator { - -/* val recognizer = TextRecognition.getClient( - ChineseTextRecognizerOptions - .Builder() - .build() - ) - - fun run(text: String, successListener: OnSuccessListener, failureListener: OnFailureListener) { - recognizer.process( - InputImage.fromBitmap( - chineseTextToImage(text), - 0) - ) - .addOnSuccessListener(successListener) - .addOnFailureListener(failureListener) - } - - *//** - * Don't forget to reclaim memory y calling bitmap.recycle() - * - * @author https://stackoverflow.com/a/18077318/3649629 - * *//* - private fun chineseTextToImage(text: String): Bitmap { - val bounds = Rect() - val textPaint: TextPaint = object : TextPaint() { - init { - color = Color.WHITE - textAlign = Align.LEFT - textSize = 20f - isAntiAlias = true - } - } - textPaint.getTextBounds(text, 0, text.length, bounds) - val textLayout = StaticLayout( - text, textPaint, - bounds.width(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false - ) - var maxWidth = -1 - for (i in 0 until textLayout.lineCount) { - if (maxWidth < textLayout.getLineWidth(i)) { - maxWidth = textLayout.getLineWidth(i).toInt() - } - } - val bmp = Bitmap.createBitmap(maxWidth, textLayout.height, Bitmap.Config.ARGB_8888) - bmp.eraseColor(Color.BLACK) // just adding black background - - val canvas = Canvas(bmp) - textLayout.draw(canvas) - - return bmp - } - - *//** - * Memory-map the model file in Assets. - * - * @ref https://blog.tensorflow.org/2018/03/using-tensorflow-lite-on-android.html - * *//* - @Throws(IOException::class) - private fun loadModelFile(activity: Activity): MappedByteBuffer? { - val fileDescriptor = activity.assets.openFd(getModelPath()) - val inputStream = FileInputStream(fileDescriptor.fileDescriptor) - val fileChannel = inputStream.channel - val startOffset = fileDescriptor.startOffset - val declaredLength = fileDescriptor.declaredLength - return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) - } - - private fun getModelPath(): String { - return "" - }*/ - - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ml/PlainTranslator.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ml/PlainTranslator.kt deleted file mode 100644 index 4aebb5b..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ml/PlainTranslator.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.github.gelassen.wordinmemory.ml - -import androidx.lifecycle.LifecycleOwner -import com.google.mlkit.common.model.DownloadConditions -import com.google.mlkit.nl.translate.TranslateLanguage -import com.google.mlkit.nl.translate.Translation -import com.google.mlkit.nl.translate.Translator -import com.google.mlkit.nl.translate.TranslatorOptions -import io.github.gelassen.wordinmemory.BuildConfig -import okhttp3.internal.closeQuietly -import java.lang.Exception - -open class PlainTranslator(listener: ITranslationListener?) { - - interface ITranslationListener { - fun onTranslationSuccess(translatedText: String) - fun onTranslationFailed(exception: Exception) - fun onModelDownloaded() - fun onModelDownloadFail(exception: Exception) - } - - private var chineseToEnglishTranslator: Translator - private var isTranslationModelReady: Boolean = false - - init { - val options = TranslatorOptions.Builder() - .setSourceLanguage(TranslateLanguage.CHINESE) - .setTargetLanguage(TranslateLanguage.ENGLISH) - .build() - chineseToEnglishTranslator = Translation.getClient(options) - prepare(listener) - } - - fun isTranslationModelReady(): Boolean { - return isTranslationModelReady - } - - fun prepare(listener: ITranslationListener?) { - val conditions = DownloadConditions.Builder() - .requireWifi() - .build() - chineseToEnglishTranslator.downloadModelIfNeeded(conditions) - .addOnSuccessListener { - isTranslationModelReady = true - listener?.onModelDownloaded() - } - .addOnFailureListener { exception -> listener?.onModelDownloadFail(exception)} - } - - fun translateChineseText(text: String, listener: ITranslationListener) { - chineseToEnglishTranslator.translate(text) - .addOnSuccessListener { translatedText -> listener.onTranslationSuccess(translatedText) } - .addOnFailureListener { exception -> listener.onTranslationFailed(exception) } - } - - /** - * This is MUST be called to prevent memory leaks - * */ - fun manageAutoClose(lifecycle: LifecycleOwner) { - lifecycle.lifecycle.addObserver(chineseToEnglishTranslator) - } - - fun close() { - if (BuildConfig.DEBUG) { - chineseToEnglishTranslator.close() - } else { - chineseToEnglishTranslator.closeQuietly() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ml/Translation.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ml/Translation.kt deleted file mode 100644 index ef8aae9..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ml/Translation.kt +++ /dev/null @@ -1,97 +0,0 @@ -package io.github.gelassen.wordinmemory.ml - -import android.app.Activity -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Rect -import android.text.Layout -import android.text.StaticLayout -import android.text.TextPaint -import com.google.android.gms.tasks.OnFailureListener -import com.google.android.gms.tasks.OnSuccessListener -/*import com.google.mlkit.vision.common.InputImage -import com.google.mlkit.vision.text.Text -import com.google.mlkit.vision.text.TextRecognition -import com.google.mlkit.vision.text.chinese.ChineseTextRecognizerOptions*/ -import java.io.FileInputStream -import java.io.IOException -import java.nio.MappedByteBuffer -import java.nio.channels.FileChannel - - -@Deprecated("Code is left as is for future research. Quick solution has been found by alternative approach") -class Translation { - -/* val recognizer = TextRecognition.getClient( - ChineseTextRecognizerOptions - .Builder() - .build() - ) - - fun run(text: String, successListener: OnSuccessListener, failureListener: OnFailureListener) { - recognizer.process( - InputImage.fromBitmap( - chineseTextToImage(text), - 0) - ) - .addOnSuccessListener(successListener) - .addOnFailureListener(failureListener) - } - - *//** - * Don't forget to reclaim memory y calling bitmap.recycle() - * - * @author https://stackoverflow.com/a/18077318/3649629 - * *//* - private fun chineseTextToImage(text: String): Bitmap { - val bounds = Rect() - val textPaint: TextPaint = object : TextPaint() { - init { - color = Color.WHITE - textAlign = Align.LEFT - textSize = 20f - isAntiAlias = true - } - } - textPaint.getTextBounds(text, 0, text.length, bounds) - val textLayout = StaticLayout( - text, textPaint, - bounds.width(), Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false - ) - var maxWidth = -1 - for (i in 0 until textLayout.lineCount) { - if (maxWidth < textLayout.getLineWidth(i)) { - maxWidth = textLayout.getLineWidth(i).toInt() - } - } - val bmp = Bitmap.createBitmap(maxWidth, textLayout.height, Bitmap.Config.ARGB_8888) - bmp.eraseColor(Color.BLACK) // just adding black background - - val canvas = Canvas(bmp) - textLayout.draw(canvas) - - return bmp - } - - *//** - * Memory-map the model file in Assets. - * - * @ref https://blog.tensorflow.org/2018/03/using-tensorflow-lite-on-android.html - * *//* - @Throws(IOException::class) - private fun loadModelFile(activity: Activity): MappedByteBuffer? { - val fileDescriptor = activity.assets.openFd(getModelPath()) - val inputStream = FileInputStream(fileDescriptor.fileDescriptor) - val fileChannel = inputStream.channel - val startOffset = fileDescriptor.startOffset - val declaredLength = fileDescriptor.declaredLength - return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength) - } - - private fun getModelPath(): String { - return "" - }*/ - - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/model/SplitOnWordsApiResponse.kt b/app/src/main/java/io/github/gelassen/wordinmemory/model/SplitOnWordsApiResponse.kt deleted file mode 100644 index 6503861..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/model/SplitOnWordsApiResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.gelassen.wordinmemory.model - -import com.google.gson.annotations.SerializedName - -data class SplitOnWordsApiResponse( - @SerializedName("status") var status: Int? = null, - @SerializedName("data") var data: ArrayList> = arrayListOf() -) \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/model/SplitOnWordsPayload.kt b/app/src/main/java/io/github/gelassen/wordinmemory/model/SplitOnWordsPayload.kt deleted file mode 100644 index 55e0b86..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/model/SplitOnWordsPayload.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.gelassen.wordinmemory.model - -import com.google.gson.annotations.SerializedName - -data class SplitOnWordsPayload ( - @SerializedName("text") var text: String = "" -) \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/model/SubjectToStudy.kt b/app/src/main/java/io/github/gelassen/wordinmemory/model/SubjectToStudy.kt deleted file mode 100644 index 01ea171..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/model/SubjectToStudy.kt +++ /dev/null @@ -1,111 +0,0 @@ -package io.github.gelassen.wordinmemory.model - -import android.os.Parcel -import android.os.Parcelable -import de.siegmar.fastcsv.reader.CsvRow -import io.github.gelassen.wordinmemory.storage.SubjectToStudyEntity -import org.json.JSONObject - -class SubjectToStudy() : Parcelable { - var uid: Int = 0 - lateinit var toTranslate: String - lateinit var translation: String - var isCompleted: Boolean = false - var tutorCounter: Int = 0 - - constructor(uid: Int = 0, toTranslate: String, translation: String, isCompleted: Boolean = false, tutorCounter: Int = 0) : this() { - this.uid = uid - this.toTranslate = toTranslate - this.translation = translation - this.isCompleted = isCompleted - this.tutorCounter = tutorCounter - } - constructor(parcel: Parcel) : this() { - uid = parcel.readInt() - toTranslate = parcel.readString()!! - translation = parcel.readString()!! - isCompleted = parcel.readByte() != 0.toByte() - tutorCounter = parcel.readInt() - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeInt(uid) - parcel.writeString(toTranslate) - parcel.writeString(translation) - parcel.writeByte(if (isCompleted) 1 else 0) - parcel.writeInt(tutorCounter) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): SubjectToStudy { - return SubjectToStudy(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} - -private object SubjectToStudyConst { - const val UID = "UID" - const val DATA = "DATA" - const val TO_TRANSLATE = "TO_TRANSLATE" - const val TRANSLATION = "TRANSLATION" - const val IS_COMPLETED = "IS_COMPLETED" - const val TUTOR_COUNTER = "TUTOR_COUNTER" -} - -fun SubjectToStudy.convertToJson(): String { - val result = JSONObject() - result.put(SubjectToStudyConst.UID, uid) - result.put(SubjectToStudyConst.TO_TRANSLATE, toTranslate) - result.put(SubjectToStudyConst.TRANSLATION, translation) - result.put(SubjectToStudyConst.IS_COMPLETED, isCompleted) - result.put(SubjectToStudyConst.TUTOR_COUNTER, tutorCounter) - return result.toString() -} - -fun SubjectToStudy.fromJson(subj: String): SubjectToStudy { - val json = JSONObject(subj) - val uid = json.optInt(SubjectToStudyConst.UID) - val toTranslate = json.optString(SubjectToStudyConst.TO_TRANSLATE) - val isCompleted = json.optBoolean(SubjectToStudyConst.IS_COMPLETED) - val translation = json.optString(SubjectToStudyConst.TRANSLATION) - val tutorCounter = json.optInt(SubjectToStudyConst.TUTOR_COUNTER) - return SubjectToStudy(uid, toTranslate, translation, isCompleted, tutorCounter) -} - -fun SubjectToStudy.toStorage(): SubjectToStudyEntity { - return SubjectToStudyEntity( - uid, - toTranslate, - translation, - isCompleted, - tutorCounter - ) -} - -fun SubjectToStudy.fromStorage(subjectToStudy: SubjectToStudyEntity): SubjectToStudy { - return SubjectToStudy( - subjectToStudy.uid, - subjectToStudy.subjectToTranslate, - subjectToStudy.translation, - subjectToStudy.isCompleted, - subjectToStudy.tutorCounter - ) -} - -fun SubjectToStudy.fromCsvRow(csvRow: CsvRow): SubjectToStudy { - return SubjectToStudy( - csvRow.getField(0).toInt(), - csvRow.getField(1), - csvRow.getField(2), - csvRow.getField(3).toBoolean(), - csvRow.getField(4).toInt() - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/network/ApiResponse.kt b/app/src/main/java/io/github/gelassen/wordinmemory/network/ApiResponse.kt deleted file mode 100644 index 8195800..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/network/ApiResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.gelassen.wordinmemory.network - -import com.google.gson.annotations.SerializedName - -data class ApiResponse ( - @SerializedName("payload") var payload : T, - @SerializedName("msg") var message : String -) \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/network/IApi.kt b/app/src/main/java/io/github/gelassen/wordinmemory/network/IApi.kt deleted file mode 100644 index 5a6b291..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/network/IApi.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.gelassen.wordinmemory.network - -import io.github.gelassen.wordinmemory.model.SplitOnWordsPayload -import io.github.gelassen.wordinmemory.model.SplitOnWordsApiResponse -import retrofit2.Response -import retrofit2.http.Body -import retrofit2.http.Headers -import retrofit2.http.POST - -interface IApi { - - @Headers("Content-Type: application/json; charset=utf-8") - @POST("/classify") - suspend fun splitChineseTextIntoWords( - @Body subj: SplitOnWordsPayload - ): Response -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/network/Response.kt b/app/src/main/java/io/github/gelassen/wordinmemory/network/Response.kt deleted file mode 100644 index 45a4d1e..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/network/Response.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.github.gelassen.wordinmemory.network - -sealed class Response { - data class Data(val data: T): Response() - sealed class Error: Response() { - data class Exception(val error: Throwable): Error() - data class Message(val msg: String): Error() - } - /*data class Loading(val isLoading: Boolean): Response()*/ -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/providers/DashboardProvider.kt b/app/src/main/java/io/github/gelassen/wordinmemory/providers/DashboardProvider.kt deleted file mode 100644 index da49b53..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/providers/DashboardProvider.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.github.gelassen.wordinmemory.providers - -import android.text.format.DateUtils -import android.util.Log -import io.github.gelassen.wordinmemory.App -import java.text.SimpleDateFormat -import java.util.Locale - -class DashboardProvider { - - fun isInputEmpty(input: String): Boolean { - return input.isEmpty() - } - - fun isTimeToShowDailyTraining(lastShownTime: Long, currentTime: Long) : Boolean { - Log.d(App.TAG, "isTimeToShowDailyTraining()") - if (currentTime < lastShownTime) return true // it shouldn't happened, just protect function from occasional garbage - // TODO consider to use lastShownTime.seconds.toComponents {} - return !DateUtils.isToday(lastShownTime) - } - - fun getLegacyDateDifference(fromDate: String, toDate: String, - formatter: String= "yyyy-MM-dd HH:mm:ss", - locale: Locale = Locale.getDefault()): Map { - - val fmt = SimpleDateFormat(formatter, locale) - val bgn = fmt.parse(fromDate) - val end = fmt.parse(toDate) - - val milliseconds = end.time - bgn.time - val days = milliseconds / 1000 / 3600 / 24 - val hours = milliseconds / 1000 / 3600 - val minutes = milliseconds / 1000 / 3600 - val seconds = milliseconds / 1000 - val weeks = days.div(7) - - return mapOf("days" to days, "hours" to hours, "minutes" to minutes, "seconds" to seconds, "weeks" to weeks) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/repository/NetworkRepository.kt b/app/src/main/java/io/github/gelassen/wordinmemory/repository/NetworkRepository.kt deleted file mode 100644 index f250aa1..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/repository/NetworkRepository.kt +++ /dev/null @@ -1,41 +0,0 @@ -package io.github.gelassen.wordinmemory.repository - -import android.util.Log -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.model.SplitOnWordsPayload -import io.github.gelassen.wordinmemory.model.SplitOnWordsApiResponse -import io.github.gelassen.wordinmemory.network.ApiResponse -import io.github.gelassen.wordinmemory.network.IApi -import io.github.gelassen.wordinmemory.network.Response -import java.net.HttpURLConnection - -class NetworkRepository(val api: IApi) { - - suspend fun splitChineseSentenceIntoWords(text: String): Response>> { - lateinit var result: Response>> - try { - val response = api.splitChineseTextIntoWords(subj = SplitOnWordsPayload(text)) - Log.d(App.TAG, "response headers ${response.headers()}") - Log.d(App.TAG, "Response from the backend as body ${response.body()} " + - "and as a raw ${response.raw()} ") /*(${response.raw().body!!.byteString()} and ${response.raw().message})*/ - if (isRequestOk(response)) { - val payload = response.body()!! // check if it can be consumed a second time - result = Response.Data(payload.data) - } else { - Log.d(App.TAG, "Get an error from backend ${response.errorBody()} + ${response.message()}") - result = Response.Error.Message("Something went wrong on backend") - } - } catch (ex: Exception) { - Log.e(App.TAG, "Failed to classify text with error", ex) - result = Response.Error.Exception(ex) - } - return result - } - - private fun isRequestOk(response: retrofit2.Response): Boolean { - Log.d(App.TAG, "Response from the backend ${response}") - return response.isSuccessful - && response.body()!!.status == HttpURLConnection.HTTP_OK - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/repository/StorageRepository.kt b/app/src/main/java/io/github/gelassen/wordinmemory/repository/StorageRepository.kt deleted file mode 100644 index b3bb1d0..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/repository/StorageRepository.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.gelassen.wordinmemory.repository - -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.model.toStorage -import io.github.gelassen.wordinmemory.storage.SubjectToStudyDao -import io.github.gelassen.wordinmemory.storage.SubjectToStudyEntity -import io.github.gelassen.wordinmemory.storage.toDomain -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class StorageRepository(val subjectsDao: SubjectToStudyDao) { - - suspend fun saveSubject(vararg subj: SubjectToStudy) { - val dbEntities = mutableListOf() - subj.forEach { dbEntities.add(it.toStorage()) } - subjectsDao.insertAll(*dbEntities.map { it }.toTypedArray()) - } - - suspend fun getSubjects(): Flow> { - return subjectsDao.getAll() - .map { it -> it.map { it.toDomain() } } - } - - suspend fun getSubjectsNonFlow(): List { - return subjectsDao.getAllNonFlow().map { it -> it.toDomain() } - } - - suspend fun removeSubject(subj: SubjectToStudy) { - subjectsDao.delete(subj.toStorage()) - } - - suspend fun getNonCompleteSubjectsOnly(): Flow> { - return subjectsDao.getNotCompletedOnly() - .map { it -> it.map { it.toDomain() } } - } - - suspend fun getCompleteSubjectsOnly(): Flow> { - return subjectsDao.getCompletedOnly() - .map { it -> it.map { it.toDomain() } } - } - - suspend fun getDailyPractice(): List { - return subjectsDao.getFirstTenNotCompletedAndLessTutored().map { it.toDomain() } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/storage/AppDatabase.kt b/app/src/main/java/io/github/gelassen/wordinmemory/storage/AppDatabase.kt deleted file mode 100644 index 96dd65d..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/storage/AppDatabase.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.gelassen.wordinmemory.storage - -import android.content.Context -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.room.TypeConverters -import androidx.sqlite.db.SupportSQLiteDatabase -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.storage.converters.Converters - -@Database( - version = 6, - entities = [SubjectToStudyEntity::class], - exportSchema = true, - autoMigrations = [ - AutoMigration(from = 5, to = 6) - ] -) -@TypeConverters(Converters::class) -abstract class AppDatabase: RoomDatabase() { - - abstract fun subjectToStudyDao(): SubjectToStudyDao - - companion object { - // For Singleton instantiation - @Volatile private var instance: AppDatabase? = null - - fun getInstance(context: Context): AppDatabase { - return instance ?: synchronized(this) { - instance ?: buildDatabase(context).also { instance = it } - } - } - - private fun buildDatabase(context: Context): AppDatabase { - return Room.databaseBuilder(context, AppDatabase::class.java, App.DATABASE_NAME) - .addCallback( - object : RoomDatabase.Callback() { - override fun onCreate(db: SupportSQLiteDatabase) { - super.onCreate(db) -/* val request = OneTimeWorkRequestBuilder() - .setInputData(workDataOf(KEY_FILENAME to PLANT_DATA_FILENAME)) - .build() - WorkManager.getInstance(context).enqueue(request)*/ - } - } - ) - .build() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/storage/AppQuickStorage.kt b/app/src/main/java/io/github/gelassen/wordinmemory/storage/AppQuickStorage.kt deleted file mode 100644 index d913890..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/storage/AppQuickStorage.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.gelassen.wordinmemory.storage - -import android.app.Activity -import android.content.Context - -class AppQuickStorage { - - companion object { - const val KEY_LAST_TRAINED = "KEY_LAST_TRAINED" - } - - // FIXME rewrite shared preferences to data storage API to avoid disk read penalty from StrictMode - fun saveLastTrainedTime(activity: Activity, time: Long) { - val sharedPref = activity.getPreferences(Context.MODE_PRIVATE) - sharedPref - ?.edit() - ?.putLong(KEY_LAST_TRAINED, time) - ?.apply() - } - - fun getLastTrainedTime(activity: Activity): Long { - val sharedPref = activity.getPreferences(Context.MODE_PRIVATE) - return sharedPref?.getLong(KEY_LAST_TRAINED, 0L)!! - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/storage/SubjectToStudyDao.kt b/app/src/main/java/io/github/gelassen/wordinmemory/storage/SubjectToStudyDao.kt deleted file mode 100644 index ef3980f..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/storage/SubjectToStudyDao.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.gelassen.wordinmemory.storage - -import androidx.room.* -import kotlinx.coroutines.flow.Flow - -@Dao -interface SubjectToStudyDao { - object Const { - const val TABLE_NAME: String = "Subjects" - } - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insertAll(vararg subjects: SubjectToStudyEntity) - - @Query("SELECT * FROM ${Const.TABLE_NAME}") - fun getAll(): Flow> - - @Query("SELECT * FROM ${Const.TABLE_NAME}") - suspend fun getAllNonFlow(): List - - @Delete - fun delete(subject: SubjectToStudyEntity) - - @Query("SELECT * FROM ${Const.TABLE_NAME} WHERE not completed") - fun getNotCompletedOnly(): Flow> - - @Query("SELECT * FROM ${Const.TABLE_NAME} WHERE completed") - fun getCompletedOnly(): Flow> - - @Query("DELETE FROM ${Const.TABLE_NAME}") - fun clean() - - @Query("SELECT * FROM ${Const.TABLE_NAME} WHERE not completed ORDER BY tutorCounter ASC LIMIT 10") - fun getFirstTenNotCompletedAndLessTutored(): List -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/storage/SubjectToStudyEntity.kt b/app/src/main/java/io/github/gelassen/wordinmemory/storage/SubjectToStudyEntity.kt deleted file mode 100644 index 089d6cd..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/storage/SubjectToStudyEntity.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.gelassen.wordinmemory.storage - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey -import io.github.gelassen.wordinmemory.model.SubjectToStudy - -@Entity(tableName = SubjectToStudyDao.Const.TABLE_NAME) -data class SubjectToStudyEntity( - @PrimaryKey(autoGenerate = true) val uid: Int, - @ColumnInfo(name = "data") val subjectToTranslate: String, - @ColumnInfo(name = "translation") val translation: String, - @ColumnInfo(name = "completed") val isCompleted: Boolean, - @ColumnInfo(name = "tutorCounter", defaultValue = "0") val tutorCounter: Int -) - -fun SubjectToStudyEntity.toDomain(): SubjectToStudy { - return SubjectToStudy( - uid, - subjectToTranslate, - translation, - isCompleted, - tutorCounter - ) -} - -fun SubjectToStudyEntity.fromDomain(subjectToStudy: SubjectToStudy): SubjectToStudyEntity { - return SubjectToStudyEntity( - subjectToStudy.uid, - subjectToStudy.toTranslate, - subjectToStudy.translation, - subjectToStudy.isCompleted, - subjectToStudy.tutorCounter - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/storage/converters/Converters.kt b/app/src/main/java/io/github/gelassen/wordinmemory/storage/converters/Converters.kt deleted file mode 100644 index 1874793..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/storage/converters/Converters.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.gelassen.wordinmemory.storage.converters - -import androidx.room.TypeConverter -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.model.convertToJson -import io.github.gelassen.wordinmemory.model.fromJson - -class Converters { - - @TypeConverter - fun subjectToStudyFromDomainToStorage(subj: SubjectToStudy): String { - return subj.convertToJson() - } - - @TypeConverter - fun subjectToStudyFromStorageToDomain(str: String): SubjectToStudy { - return SubjectToStudy() - .fromJson(str) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/MainActivity.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/MainActivity.kt deleted file mode 100644 index 3daef6b..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/MainActivity.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.gelassen.wordinmemory.ui - -import android.os.Bundle -import android.util.Log -import androidx.appcompat.app.AppCompatActivity -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.ui.tutoring.TutoringPartOneFragment -import io.github.gelassen.wordinmemory.utils.Qualifier - -class MainActivity: AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - supportFragmentManager - .beginTransaction() - .add(R.id.container, TutoringPartOneFragment.newInstance()) - .commit() - - Log.d(App.TAG, "Is screen big enough? ${Qualifier().isScreenBigEnough(this)}") - } - - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/addnewrecord/AddNewRecordFragment.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/addnewrecord/AddNewRecordFragment.kt deleted file mode 100644 index e4da6ce..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/addnewrecord/AddNewRecordFragment.kt +++ /dev/null @@ -1,52 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.addnewrecord - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import io.github.gelassen.wordinmemory.AppApplication -import io.github.gelassen.wordinmemory.databinding.FragmentDashboardBinding -import io.github.gelassen.wordinmemory.di.ViewModelFactory -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardFragment -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardViewModel -import javax.inject.Inject - -@Deprecated(message = "we would not need a separate fragment if we would put all items immediately into db") -class AddNewRecordFragment: Fragment() { - - companion object { - - const val TAG = "AddNewRecordFragment" - - fun newInstance() : Fragment { - return AddNewRecordFragment() - } - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: NewRecordViewModel - - protected lateinit var binding: FragmentDashboardBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(false); - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - (requireActivity().application as AppApplication).getComponent().inject(this) - - viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(NewRecordViewModel::class.java) - binding = FragmentDashboardBinding.inflate(inflater, container, false) - binding.isTutoringMode = false - return binding.root - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/addnewrecord/NewRecordViewModel.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/addnewrecord/NewRecordViewModel.kt deleted file mode 100644 index 5bfb00b..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/addnewrecord/NewRecordViewModel.kt +++ /dev/null @@ -1,142 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.addnewrecord - -import android.app.Application -import android.util.Log -import androidx.databinding.ObservableField -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.asFlow -import androidx.lifecycle.viewModelScope -import androidx.work.WorkInfo -import androidx.work.WorkManager -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.backgroundjobs.AddNewRecordWorker -import io.github.gelassen.wordinmemory.backgroundjobs.BaseWorker -import io.github.gelassen.wordinmemory.backgroundjobs.getWorkRequest -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.repository.StorageRepository -import io.github.gelassen.wordinmemory.ui.dashboard.StateFlag -import io.github.gelassen.wordinmemory.utils.Validator -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import java.util.Queue -import java.util.concurrent.ConcurrentLinkedQueue -import javax.inject.Inject - -data class Model( - val toTranslate: String = "", - val sentenceInWordsForTranslation: Queue = ConcurrentLinkedQueue(), - val sentenceInWordsWithTranslation: Queue> = ConcurrentLinkedQueue(), - val sentenceInWordsWithTranslationAndPinyin: Queue> = ConcurrentLinkedQueue(), - val isLoading: Boolean = false, - val errors: List = mutableListOf(), - val messages: List = mutableListOf(), - val status: StateFlag = StateFlag.NONE -) -class NewRecordViewModel - @Inject constructor( - val app: Application, - val storageRepository: StorageRepository - ) - : AndroidViewModel(app) { - - private val state: MutableStateFlow = MutableStateFlow(Model()) - val uiState: StateFlow = state - .asStateFlow() - .stateIn(viewModelScope, SharingStarted.Eagerly, state.value) - - private val validator = Validator() - - val wordToTranslate: ObservableField = ObservableField("") - - val translation: ObservableField = ObservableField("") - - fun start() { - viewModelScope.launch { - if (!validator.isAllowedWordOrSentence(wordToTranslate.get()!!)) { - addError(app.getString(R.string.msg_error_not_valid_new_record)) - return@launch - } - val text = wordToTranslate.get()!! - val workManager = WorkManager.getInstance(app) - val workRequest = workManager.getWorkRequest(AddNewRecordWorker.Builder.build(text)) - workManager.enqueue(workRequest) - workManager - .getWorkInfoByIdLiveData(workRequest.id) - .asFlow() - .onStart { state.update { state -> state.copy(isLoading = false) } } - .onCompletion { state.update { state -> state.copy(isLoading = false) } } - .collect { - when(it.state) { - WorkInfo.State.SUCCEEDED -> { - Log.d(App.TAG, "AddNewRecordWorker is succeed") - val msg = app.getString(R.string.msg_database_backup_ok) - state.update { state -> state.copy(messages = state.messages.plus(msg)) } - } - WorkInfo.State.FAILED -> { - Log.d(App.TAG, "AddNewRecordWorker is failed") - val errorMsg = it.outputData.keyValueMap.get(BaseWorker.Consts.KEY_ERROR_MSG) as String - state.update { state -> state.copy(messages = state.errors.plus(errorMsg) ) } - } - else -> { Log.d(App.TAG, "[${workRequest.javaClass.simpleName}] unexpected state on collect with state $it") } - } - } - } - } - - fun addItem() { - Log.d(App.TAG, "Add item has started") - viewModelScope.launch { - Log.d(App.TAG, "Add item coroutine has started") - if (validator.isAllowedWordOrSentence(wordToTranslate.get()!!, translation.get()!!)) { - val subject = SubjectToStudy( - 0, - wordToTranslate.get()!!, - translation = translation.get()!!, - isCompleted = false, - tutorCounter = 0 - ) - storageRepository.saveSubject(subject) - } else { - addError("You can not add an empty word or word without translation") - } - wordToTranslate.set("") - translation.set("") - } - } - - fun updateItem(subject: SubjectToStudy) { - viewModelScope.launch { - val subj = SubjectToStudy( - uid = subject.uid, - wordToTranslate.get()!!, - translation.get()!!, - subject.isCompleted, - subject.tutorCounter - ) - storageRepository.saveSubject(subj) - wordToTranslate.set("") - translation.set("") - } - } - - private fun addError(msg: String) { - state.update { state -> - state.copy(errors = state.errors.plus(msg)) - } - } - - fun removeError(errorMsg: String) { - state.update { state -> - state.copy(errors = state.errors.filter { it != errorMsg }) - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardAdapter.kt deleted file mode 100644 index f2ad450..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardAdapter.kt +++ /dev/null @@ -1,200 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.dashboard - -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import io.github.gelassen.wordinmemory.App.Companion.TAG -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.databinding.ViewItemDasboardItemBinding -import io.github.gelassen.wordinmemory.model.SubjectToStudy - - -class DashboardAdapter(val clickListener: ClickListener) : RecyclerView.Adapter() { - - interface ClickListener { - fun onClick(data: SubjectToStudy) - fun onNonComplete(selectedSubject: SubjectToStudy) - fun onComplete(selectedSubject: SubjectToStudy) - fun onLongPress(selectedSubject: SubjectToStudy) - } - - private val data: MutableList = mutableListOf() - private var isTutoring = false - - private var lastPositionForAnimation = 0 - - private var DURATION: Long = 500 - private var onAttach = true - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding = ViewItemDasboardItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolder(binding, false) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val selectedSubject = data.get(position) - holder.binding.toTranslate.text = selectedSubject.toTranslate - setAnimation(holder.itemView, position) - prepareOnCompleteClickCase(holder, selectedSubject) - prepareToTranslateClickCase(holder, selectedSubject) - prepareResponseOnSelectionCase(holder, selectedSubject) - prepareLongPressCase(holder, selectedSubject) - decorateTutoringMode(holder) - } - - override fun getItemCount(): Int { - return data.size - } - - override fun onViewDetachedFromWindow(holder: ViewHolder) { - super.onViewDetachedFromWindow(holder) - holder.itemView.clearAnimation() - } - - override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - Log.d(TAG, "onScrollStateChanged: Called $newState") - onAttach = false - super.onScrollStateChanged(recyclerView, newState) - } - }) - super.onAttachedToRecyclerView(recyclerView) - } - - fun getDataset(): MutableList { - return data - } - - fun prepareOnCompleteClickCase(holder: ViewHolder, selectedSubject: SubjectToStudy) { - holder.binding.completeIcon.setOnClickListener { - if (selectedSubject.isCompleted) { - clickListener.onNonComplete(selectedSubject) - } else { - clickListener.onComplete(selectedSubject) - } - } - } - - fun prepareToTranslateClickCase(holder: ViewHolder, selectedSubject: SubjectToStudy) { - holder.binding.root.setOnClickListener { it -> - clickListener.onClick(selectedSubject) - holder.translationIsOn = !holder.translationIsOn - if (holder.translationIsOn) { - holder.binding.toTranslate.text = selectedSubject.toTranslate + " / " + selectedSubject.translation - } else { - holder.binding.toTranslate.text = selectedSubject.toTranslate - } - } - } - - fun prepareResponseOnSelectionCase(holder: ViewHolder, selectedSubject: SubjectToStudy) { - val selectedFlag = 1 - val notSelectedFlag = 0 - if (selectedSubject.isCompleted) { - holder.binding.root.background.level = selectedFlag - holder.binding.completeIcon.background.setLevel(selectedFlag) - holder.binding.toTranslate.setTextColor( - holder.binding.root.context.resources.getColor( - R.color.disabled_text - ) - ) - } else { - holder.binding.root.background.level = notSelectedFlag - holder.binding.completeIcon.background.setLevel(notSelectedFlag) - holder.binding.toTranslate.setTextColor( - holder.binding.root.context.resources.getColor( - R.color.enabled_text - ) - ) - } - } - - fun prepareLongPressCase(holder: ViewHolder, selectedSubject: SubjectToStudy) { - holder.binding.root.setOnLongClickListener(object: View.OnLongClickListener { - override fun onLongClick(v: View?): Boolean { - clickListener.onLongPress(selectedSubject) - return true - } - - }) - } - - private fun decorateTutoringMode(holder: ViewHolder) { - val notSelectedFlag = 0 - if (isTutoring) { - holder.binding.completeIcon.visibility = if (isTutoring) View.GONE else View.VISIBLE - holder.binding.root.background.level = notSelectedFlag - holder.binding.completeIcon.visibility = View.GONE - holder.binding.toTranslate.setTextColor( - holder.binding.root.context.resources.getColor( - R.color.enabled_text - ) - ) - } - } - - fun updateData(newData: List) { - val diffCallback = DiffUtilCallback(data, newData) - val diffResult = DiffUtil.calculateDiff(diffCallback) - data.clear() - data.addAll(newData) - diffResult.dispatchUpdatesTo(this) - } - - fun turnOnTutoring(turnOn: Boolean) { - this.isTutoring = turnOn - } - - private fun setAnimation(viewToAnimate: View, pos: Int) { -/* // If the bound view wasn't previously displayed on screen, it's animated - if (position > lastPositionForAnimation) { - val animation: Animation = - AnimationUtils.loadAnimation(viewToAnimate.context, android.R.anim.fade_in) - viewToAnimate.startAnimation(animation) - lastPositionForAnimation = position - }*/ - var position = pos - if (!onAttach) { - position = -1 - } - val isNotFirstItem = position === -1 - position++ - viewToAnimate.setAlpha(0f) - val animatorSet = AnimatorSet() - val animator = ObjectAnimator.ofFloat(viewToAnimate, "alpha", 0f, 0.5f, 1.0f) - ObjectAnimator.ofFloat(viewToAnimate, "alpha", 0f).start() - animator.setStartDelay(if (isNotFirstItem) DURATION / 2 else position * DURATION / 3) - animator.duration = 500 - animatorSet.play(animator) - animator.start() - } - - inner class ViewHolder(val binding: ViewItemDasboardItemBinding, var translationIsOn: Boolean) : RecyclerView.ViewHolder(binding.root) - - class DiffUtilCallback(private val oldList: List, private val newList: List) : - DiffUtil.Callback() { - - override fun getOldListSize(): Int = oldList.size - - override fun getNewListSize(): Int = newList.size - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldList[oldItemPosition] - val newItem = newList[newItemPosition] - return oldItem.javaClass == newItem.javaClass - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val oldItem = oldList[oldItemPosition] - val newItem = newList[newItemPosition] - - return oldItem.hashCode() == newItem.hashCode() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardFragment.kt deleted file mode 100644 index 804e033..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardFragment.kt +++ /dev/null @@ -1,240 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.dashboard - -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.util.Log -import android.view.* -import android.Manifest -import android.content.pm.PackageManager -import android.provider.DocumentsContract -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.core.content.ContextCompat -import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.StaggeredGridLayoutManager -import com.google.android.material.snackbar.Snackbar -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.AppApplication -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.databinding.FragmentDashboardBinding -import io.github.gelassen.wordinmemory.di.ViewModelFactory -import io.github.gelassen.wordinmemory.dialogs.AddItemDialogProxy -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.utils.ConfigParams -import javax.inject.Inject - -open class DashboardFragment: Fragment(), - DashboardAdapter.ClickListener { - - companion object { - - const val TAG = "DashboardFragment" - - const val PERMISSION_REQUEST_CODE = 1001 - - fun newInstance() : Fragment { - return DashboardFragment() - } - } - - @Inject - lateinit var viewModelFactory: ViewModelFactory - lateinit var viewModel: DashboardViewModel - - protected lateinit var binding: FragmentDashboardBinding - - private var restoreRequestLauncher = - registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result: ActivityResult -> - Log.d(App.TAG, "restoreRequestLauncher callback is triggered ${result}") - when(result.resultCode) { - RESULT_OK -> { - Log.d(App.TAG, "Receive a result on the request for a document ${result.data!!.data!!}") - val uri = result.data!!.data!! - viewModel.restoreVocabulary(uri) - } - else -> { Log.d(App.TAG, "Haven't received a document on the request ${result.resultCode}") } - } - } - - private var createDocBackupRequestLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result: ActivityResult -> - Log.d(App.TAG, "createDocBackupRequestLauncher callback is triggered $result") - when(result.resultCode) { - RESULT_OK -> { - Log.d(App.TAG, "Receive result on CREATE_DOCUMENT request ${result.data!!.data!!}") - val uri = result.data!!.data!! - viewModel.backupVocabulary(uri) - } - else -> { Log.d(App.TAG, "Haven't received a document uri on the CREATE_DOCUMENT request ${result.resultCode}") } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setHasOptionsMenu(true); - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - (requireActivity().application as AppApplication).getComponent().inject(this) - - /** - * It is important to keep the same ViewModelStoreOwner across different fragments - * which uses the same ViewModel - * @link https://stackoverflow.com/questions/76811413/how-to-share-viewmodel-and-its-scope-across-fragments - * */ - viewModel = ViewModelProvider(requireActivity(), viewModelFactory).get(DashboardViewModel::class.java) - binding = FragmentDashboardBinding.inflate(inflater, container, false) - binding.isTutoringMode = false - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - (requireActivity() as AppCompatActivity).setSupportActionBar(binding.toolbar) - - val dr = ColorDrawable(getApiSupportColor()) - (requireActivity() as AppCompatActivity).supportActionBar!!.setBackgroundDrawable(dr) - - binding.dashboardList.layoutManager = StaggeredGridLayoutManager( - ConfigParams().getAmountOfColumnsForDashboard(requireActivity()), - StaggeredGridLayoutManager.VERTICAL - ) - binding.dashboardList.adapter = DashboardAdapter(this) - (binding.dashboardList.adapter as DashboardAdapter).turnOnTutoring(false) - binding.dashboardAddNewWord.apply { - setOnClickListener { - AddItemDialogProxy() - .show(null, childFragmentManager, requireActivity()) - } - } - - runOnStart() - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - inflater.inflate(R.menu.main_menu, menu) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when(item.itemId) { - R.id.showAll -> { - viewModel.showAll() - return true - } - R.id.showNonCompletedOnly -> { - viewModel.showNonCompletedOnly() - return true - } - R.id.backupVocabulary -> { - requestCreateDocStorageAccessFramework() - return true - } - R.id.restoreVocabulary -> { - requestRestorePermissions() - return true - } - R.id.privacyPolicy -> { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(getString(R.string.privacy_policy_endpoint)) - startActivity(intent) - return true - } - else -> { super.onOptionsItemSelected(item) } - } - } - - override fun onClick(data: SubjectToStudy) { - // no op - } - - override fun onNonComplete(selectedSubject: SubjectToStudy) { - lifecycleScope.launchWhenCreated { - viewModel.updateItem(selectedSubject, isComplete = false) - } - } - - override fun onComplete(selectedSubject: SubjectToStudy) { - lifecycleScope.launchWhenCreated { - viewModel.updateItem(selectedSubject, isComplete = true) - } - } - - override fun onLongPress(selectedSubject: SubjectToStudy) { - AddItemDialogProxy() - .show(selectedSubject, childFragmentManager, requireActivity()) - } - - @Suppress("DEPRECATION") - protected fun getApiSupportColor(): Int { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - resources.getColor(R.color.colorActionBar, requireActivity().theme) - else - resources.getColor(R.color.colorActionBar) - } - - protected open fun runOnStart() { - lifecycleScope.launchWhenStarted { - viewModel.showNonCompletedOnly() - } - listenOnModelUpdates() - } - - protected fun listenOnModelUpdates(codeOnDataCollect: ((data: List) -> Unit)? = null) { - lifecycleScope.launchWhenStarted { - viewModel.uiState.collect { it -> - (binding.dashboardList.adapter as DashboardAdapter).updateData(it.data.asReversed()) -// if (it.data.isNotEmpty()) { binding.dashboardList.scrollToPosition(0) } - binding.noContentPlaceholder.visibility = if (it.data.isEmpty()) { View.VISIBLE } else { View.GONE } - codeOnDataCollect?.invoke(it.data) - - if (it.errors.isNotEmpty()) { - var error = it.errors.first() - Snackbar.make( - binding.noContentPlaceholder, - error, - Snackbar.LENGTH_SHORT - ) - .addCallback(object : Snackbar.Callback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - super.onDismissed(transientBottomBar, event) - viewModel.removeError(error) - } - }) - .show() - } - } - } - } - - private fun requestCreateDocStorageAccessFramework() { - val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/json" - putExtra(Intent.EXTRA_TITLE, getString(R.string.backup_file_json)) - } - createDocBackupRequestLauncher.launch(intent) - } - - private fun requestRestorePermissions() { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) - intent.addCategory(Intent.CATEGORY_OPENABLE) - intent.type = "application/json" - restoreRequestLauncher.launch(intent) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardViewModel.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardViewModel.kt deleted file mode 100644 index c3f53d5..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/dashboard/DashboardViewModel.kt +++ /dev/null @@ -1,331 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.dashboard - -import android.app.Activity -import android.app.Application -import android.net.Uri -import android.util.Log -import androidx.databinding.ObservableField -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.asFlow -import androidx.lifecycle.viewModelScope -import androidx.work.WorkInfo -import androidx.work.WorkManager -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.backgroundjobs.BackupVocabularyWorker -import io.github.gelassen.wordinmemory.backgroundjobs.BaseWorker -import io.github.gelassen.wordinmemory.backgroundjobs.RestoreVocabularyWorker -import io.github.gelassen.wordinmemory.backgroundjobs.getWorkRequest -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.repository.StorageRepository -import io.github.gelassen.wordinmemory.storage.AppQuickStorage -import io.github.gelassen.wordinmemory.utils.Validator -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import javax.inject.Inject - -data class Model( - val data: List = mutableListOf(), - val isLoading: Boolean = false, - val counter: Int = 0, - val errors: List = mutableListOf(), - val messages: List = mutableListOf(), - val status: StateFlag = StateFlag.NONE -) - -enum class StateFlag { - NONE, - DATA, - TUTORING_PART_ONE, - TUTORING_PART_TWO -} - -class DashboardViewModel - @Inject constructor( - val app: Application, - val storageRepository: StorageRepository - ) - : AndroidViewModel(app) { - - companion object { - const val REQUIRED_AMOUNT_OF_ITEMS_FOR_TUTORING = 10 - } - - private val state: MutableStateFlow = MutableStateFlow(Model()) - val uiState: StateFlow = state - .asStateFlow() - .stateIn(viewModelScope, SharingStarted.Eagerly, state.value) - - val wordToTranslate: ObservableField = ObservableField("") - - val translation: ObservableField = ObservableField("") - - private var filterRequestJob: Job? = null - private val validator = Validator() - - fun addItem() { - Log.d(App.TAG, "Add item has started") - viewModelScope.launch { - Log.d(App.TAG, "Add item coroutine has started") - if (validator.isAllowedWordOrSentence(wordToTranslate.get()!!, translation.get()!!)) { - val subject = SubjectToStudy( - 0, - wordToTranslate.get()!!, - translation = translation.get()!!, - isCompleted = false, - tutorCounter = 0 - ) - storageRepository.saveSubject(subject) - } else { - addError("You can not add an empty word or word without translation") - } - wordToTranslate.set("") - translation.set("") - } - } - - override fun onCleared() { - Log.d(App.TAG, "${this.javaClass.simpleName} onCleared() call") - super.onCleared() - } - - fun updateItem(subject: SubjectToStudy) { - viewModelScope.launch { - val subj = SubjectToStudy( - uid = subject.uid, - wordToTranslate.get()!!, - translation.get()!!, - subject.isCompleted, - subject.tutorCounter - ) - storageRepository.saveSubject(subj) - wordToTranslate.set("") - translation.set("") - } - } - - fun updateItem(selectedSubject: SubjectToStudy, isComplete: Boolean) { - viewModelScope.launch { - selectedSubject.isCompleted = isComplete - storageRepository - .saveSubject(selectedSubject) - } - } - - fun showAll() { - filterRequestJob?.cancel() - filterRequestJob = viewModelScope.launch { - storageRepository - .getSubjects() - .cancellable() - .flowOn(Dispatchers.IO) - .collect { it -> - Log.d(App.TAG, "[showAll] show all result (count: ${it.size}) $it") - state.update { state -> - state.copy(data = it, status = StateFlag.DATA) - } - } - } - } - - fun showNonCompletedOnly() { - filterRequestJob?.cancel() - filterRequestJob = viewModelScope.launch { - storageRepository - .getNonCompleteSubjectsOnly() - .cancellable() - .flowOn(Dispatchers.IO) - .collect { it -> - Log.d(App.TAG, "[showAll] show not completed only result (count: ${it.size}) $it") - state.update { state -> - state.copy(data = it, status = StateFlag.DATA) - } - } - } - } - - fun addError(msg: String) { - state.update { state -> - state.copy(errors = state.errors.plus(msg)) - } - } - - fun removeError(error: String) { - state.update { state -> - state.copy(errors = state.errors.filter { it -> it != error }) - } - } - fun backupVocabulary(uriToBackupDataFile: Uri) { - viewModelScope.launch { - val workManager = WorkManager.getInstance(app) - val workRequest = workManager.getWorkRequest( - BackupVocabularyWorker.Builder.build(uriToBackupDataFile) - ) - workManager.enqueue(workRequest) - workManager - .getWorkInfoByIdLiveData(workRequest.id) - .asFlow() - .onStart { state.update { state -> state.copy(isLoading = false) } } - .onCompletion { state.update { state -> state.copy(isLoading = false) } } - .collect { - when(it.state) { - WorkInfo.State.SUCCEEDED -> { - val msg = app.getString(R.string.msg_database_backup_ok) - state.update { state -> state.copy(messages = state.messages.plus(msg)) } - } - WorkInfo.State.FAILED -> { - val errorMsg = it.outputData.keyValueMap.get(BaseWorker.Consts.KEY_ERROR_MSG) as String - state.update { state -> state.copy(messages = state.errors.plus(errorMsg) ) } - } - else -> { Log.d(App.TAG, "[${workRequest.javaClass.simpleName}] unexpected state on collect with state $it") } - } - } - - } - } - - fun restoreVocabulary(backupUri: Uri) { - viewModelScope.launch { - val workManager = WorkManager.getInstance(app) - val workRequest = workManager.getWorkRequest( - RestoreVocabularyWorker.Builder.build(backupUri) - ) - workManager.enqueue(workRequest) - workManager - .getWorkInfoByIdLiveData(workRequest.id) - .asFlow() - .onStart { state.update { state -> state.copy(isLoading = false) } } - .onCompletion { state.update { state -> state.copy(isLoading = false) } } - .collect { - when(it.state) { - WorkInfo.State.SUCCEEDED -> { -// val msg = app.getString(R.string.msg_database_backup_ok) -// state.update { state -> state.copy(messages = state.messages.plus(msg)) } - // no op, data should appear in list - Log.d(App.TAG, "Command is successfully finished") - } - WorkInfo.State.FAILED -> { - val errorMsg = it.outputData.keyValueMap.get(BaseWorker.Consts.KEY_ERROR_MSG) as String - state.update { state -> state.copy(messages = state.errors.plus(errorMsg) ) } - } - else -> { Log.d(App.TAG, "[${workRequest.javaClass.simpleName}] unexpected state on collect with state $it") } - } - } - - } - } - - suspend fun showPartOneDailyPractice() { - /* when we show only subset based on sql query, observer doesn't respond on changes in database */ -/* withContext(Dispatchers.IO) { - val dailyPractice = storageRepository.getDailyPractice() - state.update { state -> - state.copy(isLoading = false, data = dailyPractice) - } - }*/ - Log.d(App.TAG, "[start] showDailyPractice") - val itemsForPracticeAmount = 10 - filterRequestJob?.cancel() - filterRequestJob = viewModelScope.launch { - Log.d(App.TAG, "${this.javaClass.simpleName} showDailyPractice()::viewModelScope.launch {}") - storageRepository - .getNonCompleteSubjectsOnly() - .cancellable() - .map { it -> it.sortedBy { item -> item.tutorCounter } } - /*.take(itemsForPracticeAmount)*/ - .flowOn(Dispatchers.IO) - .collect { it -> - Log.d(App.TAG, "[showAll] show not completed only result (count: ${it.size}) $it") - state.update { state -> - state.copy(data = it.take(itemsForPracticeAmount), status = StateFlag.TUTORING_PART_ONE) - } - } - } - } - - suspend fun showPartTwoDailyPractice() { - /* when we show only subset based on sql query, observer doesn't respond on changes in database */ - /* withContext(Dispatchers.IO) { - val dailyPractice = storageRepository.getDailyPractice() - state.update { state -> - state.copy(isLoading = false, data = dailyPractice) - } - }*/ - Log.d(App.TAG, "[start] showPartTwoDailyPractice") - val itemsForPracticeAmount = 10 - filterRequestJob?.cancel() - filterRequestJob = viewModelScope.launch { - storageRepository - .getCompleteSubjectsOnly() - .cancellable() - .map { it -> it.sortedBy { item -> item.tutorCounter } } - .map { it -> revertBackTranslationAndSubjectToTranslate(it) } - /*.take(itemsForPracticeAmount)*/ - .flowOn(Dispatchers.IO) - .collect { it -> - Log.d(App.TAG, "[showAll] show not completed only result (count: ${it.size}) $it") - state.update { state -> - state.copy(data = it.take(itemsForPracticeAmount), status = StateFlag.TUTORING_PART_TWO) - } - } - } - } - - suspend fun completePartOneDailyPractice( - activity: Activity, - dataset: MutableList - ) { - // race condition on dataset was there, deep copy solved an issue - // https://stackoverflow.com/questions/34697828/parallel-operations-on-kotlin-collections - // https://stackoverflow.com/questions/45575516/kotlin-process-collection-in-parallel - withContext(Dispatchers.IO) { - AppQuickStorage().saveLastTrainedTime(activity, System.currentTimeMillis()) - dataset.forEach { it.tutorCounter++ } - storageRepository.saveSubject(*dataset.map { it }.toTypedArray()) - } - } - - suspend fun completePartTwoDailyPractice( - activity: Activity, - dataset: MutableList - ) { - val list = revertBackTranslationAndSubjectToTranslate(dataset.toList()) - completePartOneDailyPractice(activity, list.toMutableList()) - } - - fun clearState() { - state.update { state -> - state.copy(data = emptyList(), counter = 0) - } - } - - private fun revertBackTranslationAndSubjectToTranslate(dataset: List): List { - val data = dataset.map { it -> - val tmp = it.translation - it.translation = it.toTranslate - it.toTranslate = tmp - it - } - return data - } - - fun shallSkipPartOneTutoringScreen(): Boolean { - return state.value.status == StateFlag.TUTORING_PART_ONE - && state.value.data.isEmpty() - } - - fun shallSkipPartTwoTutoringScreen(): Boolean { - return state.value.status == StateFlag.TUTORING_PART_TWO - && state.value.data.isEmpty() - } - - fun areNotEnoughWordsForPractice(): Boolean { - return state.value.data.isNotEmpty() - && state.value.data.size < REQUIRED_AMOUNT_OF_ITEMS_FOR_TUTORING - } - - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/BaseTutoringFragment.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/BaseTutoringFragment.kt deleted file mode 100644 index ab8b60c..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/BaseTutoringFragment.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.tutoring - -import android.os.Bundle -import android.util.Log -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardAdapter -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardFragment - -abstract class BaseTutoringFragment: DashboardFragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val view = super.onCreateView(inflater, container, savedInstanceState) - binding.isTutoringMode = true - return view - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - binding.completeText.text = getString(R.string.complete_daily_practice) - binding.completeDailyPractice.apply { - setOnClickListener { - Log.d(App.TAG, "Click on complete daily practice") - // FIXME despite on extra reference to target list remain data accessible, it should - // be rewritten by using a deep copy instead of shallow copy https://stackoverflow.com/a/75096673/3649629 - val dataset = (binding.dashboardList.adapter as DashboardAdapter).getDataset() - val shallowCopyDataset = buildList { addAll(dataset) } - onCompleteDailyPractice(shallowCopyDataset.toMutableList()) - } - } - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - /* super.onCreateOptionsMenu(menu, inflater) */ - /* disable menu for this screen */ - } - - abstract fun onCompleteDailyPractice(dataset: MutableList) - - protected open fun clearModelState() { - viewModel.clearState() - } - - protected open fun finishWork() { - /** - * Should be more safe way to cancel coroutines to allow continue use scope again - * https://stackoverflow.com/a/65668544/3649629 - * */ -// viewModel.viewModelScope.coroutineContext.cancelChildren() - viewModel.clearState() - showMainScreen() - } - - protected fun showMainScreen() { - requireActivity().supportFragmentManager - .beginTransaction() - .replace(R.id.container, DashboardFragment.newInstance()) - .commit() - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/TutoringPartOneFragment.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/TutoringPartOneFragment.kt deleted file mode 100644 index e6845d8..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/TutoringPartOneFragment.kt +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.tutoring - -import android.os.Bundle -import android.util.Log -import android.view.View -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import io.github.gelassen.wordinmemory.App -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.providers.DashboardProvider -import io.github.gelassen.wordinmemory.storage.AppQuickStorage - -class TutoringPartOneFragment : BaseTutoringFragment() { - - companion object { - - fun newInstance(): Fragment { - return TutoringPartOneFragment() - } - } - - private val quickStorage: AppQuickStorage = AppQuickStorage() - private val provider: DashboardProvider = DashboardProvider() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - /*binding.completeText.text = getString(R.string.complete_daily_practice).plus(" 1 / 2")*/ - } - - override fun onDestroy() { - super.onDestroy() - Log.d(App.TAG, "onDestroy() from ${this.javaClass.simpleName}") - } - - override fun onCompleteDailyPractice(dataset: MutableList) { - lifecycleScope.launchWhenCreated { - viewModel.completePartOneDailyPractice(requireActivity(), dataset) - } - completePartOneTutoring() -// finishWork() - } - - override fun runOnStart() { - Log.d(App.TAG, "${this.javaClass.simpleName} runOnStart()") - if (provider.isTimeToShowDailyTraining( - lastShownTime = quickStorage.getLastTrainedTime(requireActivity()), - currentTime = System.currentTimeMillis())) { - lifecycleScope.launchWhenStarted { - Log.d(App.TAG, "${this.javaClass.simpleName} showDailyPractice()") - viewModel.showPartOneDailyPractice() - } - - listenOnModelUpdates() { dataset -> - Log.d(App.TAG, "${this.javaClass.simpleName} listenOnModelUpdates()") -// if (viewModel.uiState.value.status == StateFlag.TUTORING_PART_ONE) { return@listenOnModelUpdates } - if (viewModel.shallSkipPartOneTutoringScreen() - || viewModel.areNotEnoughWordsForPractice()) { - Log.d(App.TAG, "${this.javaClass.simpleName} completePartOneTutoring()") - completePartOneTutoring() -// finishWork() - } - } - } else { - finishWork() - } - } - - private fun completePartOneTutoring() { - /** - * without explicit cancellation from coroutine, it is triggered quicker rather than onDestroy() - * call (onDestroy() would cancel coroutines automatically) which leads to infinite invocation loop - * */ -// viewModel.viewModelScope.coroutineContext.cancelChildren() - viewModel.clearState() - showNextTutoringPart() -// showMainScreen() - } - - private fun showNextTutoringPart() { - requireActivity().supportFragmentManager - .beginTransaction() - .replace(R.id.container, TutoringPartTwoFragment.newInstance()) - .commit() - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/TutoringPartTwoFragment.kt b/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/TutoringPartTwoFragment.kt deleted file mode 100644 index 92711af..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/ui/tutoring/TutoringPartTwoFragment.kt +++ /dev/null @@ -1,63 +0,0 @@ -package io.github.gelassen.wordinmemory.ui.tutoring - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.lifecycle.lifecycleScope -import io.github.gelassen.wordinmemory.R -import io.github.gelassen.wordinmemory.model.SubjectToStudy -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardAdapter - -class TutoringPartTwoFragment : BaseTutoringFragment() { - - companion object { - - fun newInstance(): Fragment { - return TutoringPartTwoFragment() - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - val view = super.onCreateView(inflater, container, savedInstanceState) - binding.isTutoringMode = true - return view - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - (binding.dashboardList.adapter as DashboardAdapter).turnOnTutoring(true) - /*binding.completeText.text = getString(R.string.complete_daily_practice).plus(" 2 / 2")*/ - } - - override fun onLongPress(selectedSubject: SubjectToStudy) { - /* no ops */ - } - - override fun onCompleteDailyPractice(dataset: MutableList) { - lifecycleScope.launchWhenCreated { - viewModel.completePartTwoDailyPractice(requireActivity(), dataset) - } - finishWork() - } - - override fun runOnStart() { - - lifecycleScope.launchWhenStarted { - viewModel.showPartTwoDailyPractice() - } - - listenOnModelUpdates() { dataset -> - if (viewModel.shallSkipPartTwoTutoringScreen() - || viewModel.areNotEnoughWordsForPractice()) { - finishWork() - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/utils/ConfigParams.kt b/app/src/main/java/io/github/gelassen/wordinmemory/utils/ConfigParams.kt deleted file mode 100644 index 4012e7a..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/utils/ConfigParams.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.gelassen.wordinmemory.utils - -import android.app.Activity - -class ConfigParams { - - private val qualifier: Qualifier = Qualifier() - - fun getAmountOfColumnsForDashboard(activity: Activity): Int { - val tabletColumnsAmount = 2 - val deviceColumnsAmount = 1 - return if (qualifier.isTablet(activity)) tabletColumnsAmount else deviceColumnsAmount - } - - fun showDialogAsBottomSheet(activity: Activity): Boolean { - return qualifier.isScreenBigEnough(activity) - || qualifier.isTablet(activity) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/utils/FileUtils.kt b/app/src/main/java/io/github/gelassen/wordinmemory/utils/FileUtils.kt deleted file mode 100644 index 762c124..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/utils/FileUtils.kt +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.gelassen.wordinmemory.utils - -import android.os.Environment -import android.util.Log -import io.github.gelassen.wordinmemory.App - -class FileUtils { - - fun isExternalStorageAvailable(): Boolean { - var isExternalStorageAvailable = false - var isExternalStorageWriteable = false - val state = Environment.getExternalStorageState() - if (Environment.MEDIA_MOUNTED == state) { - isExternalStorageWriteable = true - isExternalStorageAvailable = isExternalStorageWriteable - } else if (Environment.MEDIA_MOUNTED_READ_ONLY == state) { - isExternalStorageAvailable = true - isExternalStorageWriteable = false - } else { - isExternalStorageWriteable = false - isExternalStorageAvailable = isExternalStorageWriteable - } - Log.d( - App.TAG, "External storage state availability (${isExternalStorageAvailable}) " + - "and writeability(${isExternalStorageWriteable})") - return isExternalStorageAvailable && isExternalStorageWriteable - } -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/utils/Qualifier.kt b/app/src/main/java/io/github/gelassen/wordinmemory/utils/Qualifier.kt deleted file mode 100644 index fc34828..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/utils/Qualifier.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.gelassen.wordinmemory.utils - -import android.app.Activity -import android.content.Context -import android.util.DisplayMetrics -import io.github.gelassen.wordinmemory.R - -class Qualifier() { - - companion object { - const val BASELINE_WIDTH = 1080 - const val BASELINE_HEIGHT = 1920 - const val BASELINE_DIAGONAL = 5.0f - } - - // 5inch, 1080 x 1920 - a baseline for current UX - // 7inch, - a baseline for two columns on a display - - fun isScreenBigEnough(activity: Activity): Boolean { - val displayMetrics = DisplayMetrics() - activity.windowManager.defaultDisplay.getMetrics(displayMetrics) - return isBaselineDimensionsOrHigher(displayMetrics) - && getDiagonalInInches(displayMetrics) >= BASELINE_DIAGONAL - && !isTablet(activity) - - } - - fun isTablet(context: Context): Boolean { - return context.resources.getBoolean(R.bool.isTablet) - } - - private fun isBaselineDimensionsOrHigher(displayMetrics: DisplayMetrics): Boolean { - return displayMetrics.heightPixels >= BASELINE_HEIGHT - && displayMetrics.widthPixels >= BASELINE_WIDTH - } - - private fun getDiagonalInInches(displayMetrics: DisplayMetrics): Double { - val yInches: Float = displayMetrics.heightPixels / displayMetrics.ydpi - val xInches: Float = displayMetrics.widthPixels / displayMetrics.xdpi - val diagonalInches = Math.sqrt((xInches * xInches + yInches * yInches).toDouble()) - return diagonalInches - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/github/gelassen/wordinmemory/utils/Validator.kt b/app/src/main/java/io/github/gelassen/wordinmemory/utils/Validator.kt deleted file mode 100644 index 148b6ae..0000000 --- a/app/src/main/java/io/github/gelassen/wordinmemory/utils/Validator.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.github.gelassen.wordinmemory.utils - -import android.text.TextUtils - -class Validator { - - fun isAllowedWordOrSentence(msg: String, translatedMsg: String): Boolean { - return !TextUtils.isEmpty(msg) && !TextUtils.isEmpty(translatedMsg) - } - - fun isAllowedWordOrSentence(msg: String): Boolean { - return !TextUtils.isEmpty(msg) - } - -} \ No newline at end of file diff --git a/app/src/main/res/color/selector_two_state_view.xml b/app/src/main/res/color/selector_two_state_view.xml deleted file mode 100644 index f80d041..0000000 --- a/app/src/main/res/color/selector_two_state_view.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/bg_complete.xml b/app/src/main/res/drawable/bg_complete.xml deleted file mode 100644 index 3a55a1c..0000000 --- a/app/src/main/res/drawable/bg_complete.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_dashboard_item_completed.xml b/app/src/main/res/drawable/bg_dashboard_item_completed.xml deleted file mode 100644 index d6bfd58..0000000 --- a/app/src/main/res/drawable/bg_dashboard_item_completed.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_dashboard_item_level_list.xml b/app/src/main/res/drawable/bg_dashboard_item_level_list.xml deleted file mode 100644 index 20784da..0000000 --- a/app/src/main/res/drawable/bg_dashboard_item_level_list.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_dashboard_item_notcompleted.xml b/app/src/main/res/drawable/bg_dashboard_item_notcompleted.xml deleted file mode 100644 index b10565e..0000000 --- a/app/src/main/res/drawable/bg_dashboard_item_notcompleted.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_uncomplete.xml b/app/src/main/res/drawable/bg_uncomplete.xml deleted file mode 100644 index 93da313..0000000 --- a/app/src/main/res/drawable/bg_uncomplete.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/complete_layer_list.xml b/app/src/main/res/drawable/complete_layer_list.xml deleted file mode 100644 index 416a985..0000000 --- a/app/src/main/res/drawable/complete_layer_list.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/complete_level_list.xml b/app/src/main/res/drawable/complete_level_list.xml deleted file mode 100644 index c7d4dd4..0000000 --- a/app/src/main/res/drawable/complete_level_list.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_item.xml b/app/src/main/res/drawable/ic_add_item.xml deleted file mode 100644 index 7dd17b8..0000000 --- a/app/src/main/res/drawable/ic_add_item.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_complete.xml b/app/src/main/res/drawable/ic_complete.xml deleted file mode 100644 index 32fa0ef..0000000 --- a/app/src/main/res/drawable/ic_complete.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index 6efb57c..0000000 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 68520ef..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_test.xml b/app/src/main/res/layout/activity_test.xml deleted file mode 100644 index 1d24e9d..0000000 --- a/app/src/main/res/layout/activity_test.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/add_item_fragment.xml b/app/src/main/res/layout/add_item_fragment.xml deleted file mode 100644 index fd9543b..0000000 --- a/app/src/main/res/layout/add_item_fragment.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml deleted file mode 100644 index 6a01781..0000000 --- a/app/src/main/res/layout/fragment_dashboard.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_item_dasboard_item.xml b/app/src/main/res/layout/view_item_dasboard_item.xml deleted file mode 100644 index b9edfaf..0000000 --- a/app/src/main/res/layout/view_item_dasboard_item.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml deleted file mode 100644 index 2dbab16..0000000 --- a/app/src/main/res/menu/main_menu.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 70ff12c..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index e8fc270..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 0885e19..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 964b631..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 5ec4f5f..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 25a4d78..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 7a0a6a3..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 93d0bb1..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 9d17fbd..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index f4d6228..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml deleted file mode 100644 index b1ad84e..0000000 --- a/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/values-sw600dp/attr.xml b/app/src/main/res/values-sw600dp/attr.xml deleted file mode 100644 index d3a0e92..0000000 --- a/app/src/main/res/values-sw600dp/attr.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - true - \ No newline at end of file diff --git a/app/src/main/res/values/attr.xml b/app/src/main/res/values/attr.xml deleted file mode 100644 index f855de6..0000000 --- a/app/src/main/res/values/attr.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - false - \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml deleted file mode 100644 index fa6b566..0000000 --- a/app/src/main/res/values/attrs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index 7c54676..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - #808080 - @color/white - - - #FF0023FF - #002366 - @android:color/darker_gray - #002C96 - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #AA000000 - #FFFFFFFF - - \ No newline at end of file diff --git a/app/src/main/res/values/config.xml b/app/src/main/res/values/config.xml deleted file mode 100644 index 3d3591b..0000000 --- a/app/src/main/res/values/config.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - WordsInMemory - WordsInMemory-vocabulary.csv - WordsInMemory-vocabulary.json - - - - http://192.168.1.19:80 - false - \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml deleted file mode 100644 index 9f97cec..0000000 --- a/app/src/main/res/values/dimen.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - 16dp - - 12dp - - - 2dp - 24sp - \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index e39d1dd..0000000 --- a/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #002366 - \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml deleted file mode 100644 index 43c57ee..0000000 --- a/app/src/main/res/values/ids.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index 54eea7c..0000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - Word In Memory - - - Add new word - Enter a foreign word or sentence. - Enter the translation. - You have not added yet any word to study. Please tap on the + button. - Complete - - - Show all - Show only not completed - Backup vocabulary - Restore vocabulary - Privacy policy - - - Your vocabulary has been exported into a separate file on the SD card. - New record has been added - New record should not be empty. Have you entered a record on Chinese? - - - Add new word - OK - @android:string/cancel - https://gelassen.github.io/blog/2020/08/25/google-play-privacy-policy.html - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index 629c8f2..0000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml deleted file mode 100644 index fa0f996..0000000 --- a/app/src/main/res/xml/backup_rules.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml deleted file mode 100644 index 9ee9997..0000000 --- a/app/src/main/res/xml/data_extraction_rules.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/test/java/io/github/gelassen/wordinmemory/DashboardViewModel.kt b/app/src/test/java/io/github/gelassen/wordinmemory/DashboardViewModel.kt deleted file mode 100644 index eb5ff58..0000000 --- a/app/src/test/java/io/github/gelassen/wordinmemory/DashboardViewModel.kt +++ /dev/null @@ -1,47 +0,0 @@ -package io.github.gelassen.wordinmemory - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import io.github.gelassen.wordinmemory.repository.StorageRepository -import io.github.gelassen.wordinmemory.ui.dashboard.DashboardViewModel -import kotlinx.coroutines.runBlocking -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.Mock -import org.mockito.MockitoAnnotations - -class DashboardViewModel { - - // Executes each task synchronously using Architecture Components. - @get:Rule - var instantExecutorRule = InstantTaskExecutorRule() - - @get:Rule - var mainCoroutineRule = MainCoroutineRule() - - - @Mock - lateinit var storageRepository: StorageRepository - - private lateinit var autoCloseable: AutoCloseable - - private lateinit var subj: DashboardViewModel - - @Before - fun setUp() { - autoCloseable = MockitoAnnotations.openMocks(this) - -// subj = DashboardViewModel(storageRepository) - } - - @After - fun tearDown() { - autoCloseable.close() - } - - @Test - fun `on getWords() call when there are data in db returns all data`() = runBlocking { - // TODO complete me - } -} \ No newline at end of file diff --git a/app/src/test/java/io/github/gelassen/wordinmemory/DateAndTimeApiUnitTest.kt b/app/src/test/java/io/github/gelassen/wordinmemory/DateAndTimeApiUnitTest.kt deleted file mode 100644 index 8ec9927..0000000 --- a/app/src/test/java/io/github/gelassen/wordinmemory/DateAndTimeApiUnitTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.gelassen.wordinmemory - -import org.junit.Assert -import org.junit.Test -import kotlin.time.Duration.Companion.seconds - -class DateAndTimeApiUnitTest { - - @Test - fun checkTimeComponents_defaultCase_shownTotalAmountOfDaysAndHours() { - val time = 1690897602699//System.currentTimeMillis() - System.out.println("Time $time") - time.seconds.toComponents { days: Long, hours: Int, minutes: Int, seconds: Int, nanoseconds: Int -> - System.out.println("days ${days} ${hours}:${minutes}:${seconds}") - Assert.assertEquals(days, 19570574) - Assert.assertEquals(hours, 2) - } - } -} \ No newline at end of file diff --git a/app/src/test/java/io/github/gelassen/wordinmemory/ExampleUnitTest.kt b/app/src/test/java/io/github/gelassen/wordinmemory/ExampleUnitTest.kt deleted file mode 100644 index 466e6e4..0000000 --- a/app/src/test/java/io/github/gelassen/wordinmemory/ExampleUnitTest.kt +++ /dev/null @@ -1,125 +0,0 @@ -package io.github.gelassen.wordinmemory - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } - - @Test - fun testShallowCopyInKotlin_onCleanUpOriginArray_copiedItemRemainTheSame() { - val dataset = mutableListOf( - TestItem("Jane", 12), - TestItem("Kein", 42), - TestItem("MkLee", 7) - ) - val deepCopyDataset = buildList { addAll(dataset) } - - dataset.clear() - - val originDatasetSize = 3 - assertEquals(originDatasetSize, deepCopyDataset.size) - } - - @Test - fun testShallowCopyInKotlin_onChangeItemInOriginArray_copiedItemsHaveBeenChanged() { - val janeRecord = TestItem("Jane", 12) - val keinRecord = TestItem("Kein", 42) - val mcLeeRecord = TestItem("McLee", 7) - val dataset = mutableListOf(janeRecord, keinRecord, mcLeeRecord) - val deepCopyDataset = buildList { addAll(dataset) } - - val modifiedOriginJaneRecord = dataset.get(0) - modifiedOriginJaneRecord.name = "Bear" - dataset.set(0, modifiedOriginJaneRecord) - dataset.set(1, TestItem("Tiger", 24)) - dataset.set(2, TestItem("Lion", 0)) - - val originDatasetSize = 3 - assertEquals(originDatasetSize, dataset.size) - assertEquals(janeRecord, deepCopyDataset.get(0)) - assertNotEquals("Jane", deepCopyDataset.get(0).name) - } - - @Test - fun testDeepCopyInKotlinForDataClass_onChangeItemInOriginObject_copiedObjectRemainTheSame() { - val janeRecord = TestItemDataClass("Jane", 12)//TestItem("Jane", 12) - val deepCopyJaneRecord = mutableListOf(janeRecord).map { it.copy() } - - janeRecord.name = "Bear" - - assertEquals("Bear", janeRecord.name) - assertNotEquals("Bear", deepCopyJaneRecord.get(0).name) - } - - @Test - fun testDeepCopyInKotlin_onChangeItemInOriginObject_copiedObjectRemainTheSame() { - val janeRecord = TestItem("Jane", 12) - val deepCopyJaneRecord = janeRecord.deepCopy() - - janeRecord.name = "Bear" - - assertEquals("Bear", janeRecord.name) - assertNotEquals("Bear", deepCopyJaneRecord.name) - } - - @Test - fun testShallowCopyOfDataClass_onChangeRecord_copiedRecordChangedToo() { - val subj = TestItemDataClass("Jane", 12) - val shallowCopySubj = subj.copy() - - subj.name = "Bear" - - assertEquals("Bear", subj.name) - assertEquals("Bear", shallowCopySubj.name) // FIXME crash here, it behaves like deep copy - } - - - private data class TestItemDataClass ( - var name: String, var count: Int - ) - private class TestItem { - var name: String - var count: Int - - constructor(name: String, count: Int) { - this.name = name - this.count = count - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TestItem - - if (name != other.name) return false - if (count != other.count) return false - - return true - } - - override fun hashCode(): Int { - var result = name.hashCode() - result = 31 * result + count - return result - } - - fun deepCopy(): TestItem { - return TestItem( - name = this.name, - count = Integer.valueOf(this.count) - ) - } - - } -} \ No newline at end of file diff --git a/app/src/test/java/io/github/gelassen/wordinmemory/MainCoroutineRule.kt b/app/src/test/java/io/github/gelassen/wordinmemory/MainCoroutineRule.kt deleted file mode 100644 index 561ed28..0000000 --- a/app/src/test/java/io/github/gelassen/wordinmemory/MainCoroutineRule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.gelassen.wordinmemory - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineScope -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.rules.TestWatcher -import org.junit.runner.Description - -@ExperimentalCoroutinesApi -class MainCoroutineRule(val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()): - TestWatcher(), - TestCoroutineScope by TestCoroutineScope(dispatcher) { - - override fun starting(description: Description?) { - super.starting(description) - Dispatchers.setMain(dispatcher) - } - - override fun finished(description: Description?) { - super.finished(description) - cleanupTestCoroutines() - Dispatchers.resetMain() - } -} diff --git a/assets/7inch tablet/Screenshot_20230627-113520.png b/assets/7inch tablet/Screenshot_20230627-113520.png deleted file mode 100644 index e85b8bb..0000000 Binary files a/assets/7inch tablet/Screenshot_20230627-113520.png and /dev/null differ diff --git a/assets/7inch tablet/Screenshot_20230627-113539.png b/assets/7inch tablet/Screenshot_20230627-113539.png deleted file mode 100644 index 3b65a5d..0000000 Binary files a/assets/7inch tablet/Screenshot_20230627-113539.png and /dev/null differ diff --git a/assets/7inch tablet/Screenshot_20230627-113555.png b/assets/7inch tablet/Screenshot_20230627-113555.png deleted file mode 100644 index 06c7f1f..0000000 Binary files a/assets/7inch tablet/Screenshot_20230627-113555.png and /dev/null differ diff --git a/assets/7inch tablet/Screenshot_20230627-113624.png b/assets/7inch tablet/Screenshot_20230627-113624.png deleted file mode 100644 index a2e64b6..0000000 Binary files a/assets/7inch tablet/Screenshot_20230627-113624.png and /dev/null differ diff --git a/assets/7inch tablet/Screenshot_20230627-113649.png b/assets/7inch tablet/Screenshot_20230627-113649.png deleted file mode 100644 index 26f355c..0000000 Binary files a/assets/7inch tablet/Screenshot_20230627-113649.png and /dev/null differ diff --git a/assets/Word In Memory - WIM.png b/assets/Word In Memory - WIM.png deleted file mode 100644 index 9f9839d..0000000 Binary files a/assets/Word In Memory - WIM.png and /dev/null differ diff --git a/assets/check-mark-selected.png b/assets/check-mark-selected.png deleted file mode 100644 index b74dcf5..0000000 Binary files a/assets/check-mark-selected.png and /dev/null differ diff --git a/assets/check-mark.png b/assets/check-mark.png deleted file mode 100644 index 1bbb83c..0000000 Binary files a/assets/check-mark.png and /dev/null differ diff --git a/assets/phone/Screenshot_20230627_112701_Word In Memory.jpg b/assets/phone/Screenshot_20230627_112701_Word In Memory.jpg deleted file mode 100644 index 5c34d34..0000000 Binary files a/assets/phone/Screenshot_20230627_112701_Word In Memory.jpg and /dev/null differ diff --git a/assets/phone/Screenshot_20230627_112714_Word In Memory.jpg b/assets/phone/Screenshot_20230627_112714_Word In Memory.jpg deleted file mode 100644 index 1b3e0cb..0000000 Binary files a/assets/phone/Screenshot_20230627_112714_Word In Memory.jpg and /dev/null differ diff --git a/assets/phone/Screenshot_20230627_112724_Word In Memory.jpg b/assets/phone/Screenshot_20230627_112724_Word In Memory.jpg deleted file mode 100644 index 3e1287b..0000000 Binary files a/assets/phone/Screenshot_20230627_112724_Word In Memory.jpg and /dev/null differ diff --git a/assets/phone/Screenshot_20230627_112734_Word In Memory.jpg b/assets/phone/Screenshot_20230627_112734_Word In Memory.jpg deleted file mode 100644 index 1d87b21..0000000 Binary files a/assets/phone/Screenshot_20230627_112734_Word In Memory.jpg and /dev/null differ diff --git a/assets/phone/Screenshot_20230627_112908_Word In Memory.jpg b/assets/phone/Screenshot_20230627_112908_Word In Memory.jpg deleted file mode 100644 index dfd207d..0000000 Binary files a/assets/phone/Screenshot_20230627_112908_Word In Memory.jpg and /dev/null differ diff --git a/assets/play_store.png b/assets/play_store.png deleted file mode 100644 index 8c2e1da..0000000 Binary files a/assets/play_store.png and /dev/null differ diff --git a/assets/samsung_s20_main.png b/assets/samsung_s20_main.png new file mode 100644 index 0000000..b62b99c Binary files /dev/null and b/assets/samsung_s20_main.png differ diff --git a/backend/.gitignore b/backend/.gitignore deleted file mode 100644 index e985853..0000000 --- a/backend/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.vercel diff --git a/backend/.vercel/README.txt b/backend/.vercel/README.txt deleted file mode 100644 index 525d8ce..0000000 --- a/backend/.vercel/README.txt +++ /dev/null @@ -1,11 +0,0 @@ -> Why do I have a folder named ".vercel" in my project? -The ".vercel" folder is created when you link a directory to a Vercel project. - -> What does the "project.json" file contain? -The "project.json" file contains: -- The ID of the Vercel project that you linked ("projectId") -- The ID of the user or team your Vercel project is owned by ("orgId") - -> Should I commit the ".vercel" folder? -No, you should not share the ".vercel" folder with anyone. -Upon creation, it will be automatically added to your ".gitignore" file. diff --git a/backend/.vercel/project.json b/backend/.vercel/project.json deleted file mode 100644 index fd7a182..0000000 --- a/backend/.vercel/project.json +++ /dev/null @@ -1 +0,0 @@ -{"orgId":"Vn9tFMRQNyTLvw7GqWCkV7q7","projectId":"prj_IItcAX8fL33czveInCZKG00BrjNY"} \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile deleted file mode 100644 index 4dc26c4..0000000 --- a/backend/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM python:3.11 - -WORKDIR /deployment - -COPY ./requirements.txt /deployment/requirements.txt - -RUN pip install --no-cache-dir --upgrade -r /deployment/requirements.txt - -ENV PYTHONPATH /deployment/src - -COPY ./src /deployment/src - -CMD ["uvicorn", "src.start:app", "--host", "0.0.0.0", "--port", "80"] - diff --git a/backend/README.txt b/backend/README.txt deleted file mode 100644 index 9948797..0000000 --- a/backend/README.txt +++ /dev/null @@ -1,21 +0,0 @@ -### Deploy backend - -Run as a plain python server: - -1. $ cd src/ -2. $ uvicorn start:app --host 0.0.0.0 --port 8000 --reload - -(app.py is WSGI version of server which is required by some web hostings with a free quota; by default ASGI version is used which is implemented in start.py) - -Run via docker: (takes ~6 GB, plus 400 MB for downloaded pytorch model; on specialized hosting it is less as they usually have many things pre-installed) - -1. $ docker image build -t wim-backend . -2. $ docker run -d --name wim-backend-container -p 80:80 --net host wim-backend - -(all next container launches are done via command $ docker container start wim-backend-container) - -Command for test deployment: - -$ curl --header "Content-Type: application/json" --request POST --data '{"text" : "之后你看看了我的出版请告诉我你认为什么"}' http://127.0.0.1:8000/classify - -Sample output: [["之后","你","看看","了","我","的","出版","请","告诉","我","你","认为","什么"]] \ No newline at end of file diff --git a/backend/__pycache__/app.cpython-310.pyc b/backend/__pycache__/app.cpython-310.pyc deleted file mode 100644 index 3dfe752..0000000 Binary files a/backend/__pycache__/app.cpython-310.pyc and /dev/null differ diff --git a/backend/__pycache__/model.cpython-310.pyc b/backend/__pycache__/model.cpython-310.pyc deleted file mode 100644 index 3a54b62..0000000 Binary files a/backend/__pycache__/model.cpython-310.pyc and /dev/null differ diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index 0c0d4f7..0000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ - -ckip-transformers -fastapi -uvicorn[standard] -a2wsgi diff --git a/backend/src/__pycache__/model.cpython-310.pyc b/backend/src/__pycache__/model.cpython-310.pyc deleted file mode 100644 index 8d94b65..0000000 Binary files a/backend/src/__pycache__/model.cpython-310.pyc and /dev/null differ diff --git a/backend/src/__pycache__/start.cpython-310.pyc b/backend/src/__pycache__/start.cpython-310.pyc deleted file mode 100644 index 24ae9d4..0000000 Binary files a/backend/src/__pycache__/start.cpython-310.pyc and /dev/null differ diff --git a/backend/src/app.py b/backend/src/app.py deleted file mode 100644 index c463337..0000000 --- a/backend/src/app.py +++ /dev/null @@ -1,24 +0,0 @@ -from flask import Flask, request, jsonify -from model import ChineseTextClassifier - -app = Flask(__name__) - -model = ChineseTextClassifier() - -@app.route("/") -def hello_world(): - return "

Hello, World!

" - -@app.route('/classify', methods=['POST']) -def classify(): - input_json = request.get_json(force=True) - # force=True, above, is necessary if another developer - # forgot to set the MIME type to 'application/json' - # print('data from client:', input_json) - # dictToReturn = {'answer':42} - # return jsonify(dictToReturn) - test = model.run_single_word_segmentation(input_json.text) - return { "classified_text": test } - -if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file diff --git a/backend/src/model.py b/backend/src/model.py deleted file mode 100644 index 6b8fc22..0000000 --- a/backend/src/model.py +++ /dev/null @@ -1,55 +0,0 @@ -""" Chinese text classifier -@ref https://ckip-transformers.readthedocs.io/en/stable/main/readme.html#git -""" -import asyncio -from ckip_transformers.nlp import ( - CkipWordSegmenter, - # CkipPosTagger, - # CkipNerChunker -) - - -class ChineseTextClassifier: - - def __init__(self): - - self.ws_driver = CkipWordSegmenter(model="bert-base") - self.lock = asyncio.Lock() - # self.pos_driver = CkipPosTagger(model="bert-base") - # self.ner_driver = CkipNerChunker(model="bert-base") - - async def run_single_word_segmentation(self, text): - async with self.lock: - assert(isinstance(text, list) == True) - ws = self.ws_driver(text, use_delim=True) - return ws - - # def run_pipeline(self, text): - # assert(isinstance(text, list) == True) - # ws = self.ws_driver(text, use_delim=True) - # pos = self.pos_driver(ws, use_delim=True) - # ner = self.ner_driver(text, use_delim=True) - # print(ws) - # print(pos) - # print(ner) - # return self._prepare_results(ws, pos, ner) - - # def _prepare_results(self, ws, pos, ner): - # res = [] - # for sentence_ws, sentence_pos, sentence_ner in zip(ws, pos, ner): - # packed = self._combine_results(sentence_ws, sentence_pos) - # res.append(packed) - # return "\u3000".join(res) - - # def _combine_results(self, sentence_ws, sentence_pos): - # assert len(sentence_ws) == len(sentence_pos) - # res = [] - # for word_ws, word_pos in zip(sentence_ws, sentence_pos): - # res.append(f"{word_ws}({word_pos})") - # return "\u3000".join(res) - -# test script block -# model = ChineseTextClassifier() -# classified = model.run_single_word_segmentation(["之后你看看了我的出版请告诉我你认为什么"]) -# print(classified) - diff --git a/backend/src/start.py b/backend/src/start.py deleted file mode 100644 index a0d3eca..0000000 --- a/backend/src/start.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Union - -from typing import Optional, Any -from fastapi import FastAPI -from fastapi.responses import JSONResponse -from pydantic import BaseModel -from model import ChineseTextClassifier - -app = FastAPI() - -model = ChineseTextClassifier() - -class TextForClassification(BaseModel): - text: str - -@app.get("/") -def read_root(): - return {"Hello": "World"} - -@app.post("/classify") -async def classify(payload: TextForClassification): - result = await model.run_single_word_segmentation([payload.text]) - return get_response(True, result) - -# ref. https://pypi.org/project/fastapi-queue/ -def get_response(success_status: bool, result: Any) -> JSONResponse | dict: - if success_status: - return {"status": 200, "data": result} - if result == -1: - return JSONResponse(status_code=503, content="Service Temporarily Unavailable") - else: - return JSONResponse(status_code=500, content="Internal Server Error") diff --git a/backend/src/vercel.json b/backend/src/vercel.json deleted file mode 100644 index c563ae7..0000000 --- a/backend/src/vercel.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": 2, - "builds": [ - { "src": "start.py", "use": "@vercel/python" } - ], - "routes": [ - { "src": "/(.*)", "dest": "/start.py" } - ] - } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index f3a5ed0..0000000 --- a/build.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id("com.android.application") version "8.1.0-rc01" apply false - id("com.android.library") version "8.1.0-rc01" apply false - id("org.jetbrains.kotlin.android") version "1.7.10" apply false -} - -tasks.register("clean", Delete::class) { - delete(rootProject.buildDir) -} diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 022338b..0000000 --- a/gradle.properties +++ /dev/null @@ -1,25 +0,0 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true -android.defaults.buildfeatures.buildconfig=true -android.nonFinalResIds=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c..0000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 4ead9e9..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Nov 14 10:27:38 MSK 2022 -distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew deleted file mode 100755 index 4f906e0..0000000 --- a/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index ac1b06f..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/index.html b/index.html new file mode 100644 index 0000000..a0129d6 --- /dev/null +++ b/index.html @@ -0,0 +1,19 @@ + + + + + + App Landing Page + + + +
+
+ Smartphone with App +
+ +
+ + \ No newline at end of file diff --git a/nano b/nano deleted file mode 100644 index e69de29..0000000 diff --git a/readme.md b/readme.md deleted file mode 100644 index 8b76337..0000000 --- a/readme.md +++ /dev/null @@ -1,59 +0,0 @@ -# Publications - -Case study: automatic Chinese text word segmentation and translation - -# Dev email address -words.inmemory.dev@yandex.com - -# User Stories: - -[done] As a user I want to enter a foreign word or sentence with its translation. - -[done] As a user I want the word I have enter to be cached in app. - -As a user I want in case of sentence app split it into words. - -[done] As a user I want app shows me this words or sentences on the dashboard. - -[done] As a user I do not want to see a word translation when I see this word. - -[done] As a user I want to see translation when I tap on the word. - -[done] As a user I want to mark word as completed when I read it without translation. - -[done] As a user I want to have completed words to be shadowed (less visible compare to not completed words). - -[done] As a user I want to filter words by its flag "completed" or "uncompleted". - -[done] As a user I want to have non default launcher icon. - -[done] As a user I want to modify translation of word or sentence. - -## Deployment steps to support automatic Chinese text word segmentation, translation, pinyin extension - - -Run as a plain python server: - -1. $ cd src/ -2. $ uvicorn start:app --host 0.0.0.0 --port 8000 --reload - -(app.py is WSGI version of server which is required by some web hostings with a free quota; by default ASGI version is used which is implemented in start.py) - -Run via docker: (takes ~6 GB, plus 400 MB for downloaded pytorch model; on specialized hosting it is less as they usually have many things pre-installed) - -1. $ docker image build -t wim-backend . -2. $ docker run -d --name wim-backend-container -p 80:80 --net host wim-backend - -(all next container launches are done via command $ docker container start wim-backend-container) - -Command for test deployment: - -$ curl --header "Content-Type: application/json" --request POST --data '{"text" : "之后你看看了我的出版请告诉我你认为什么"}' http://127.0.0.1:8000/classify - -Sample output: [["之后","你","看看","了","我","的","出版","请","告诉","我","你","认为","什么"]] - -Update ```config.xml``` with endpoint ip address and backend support flag into mobile client: -``` - http://192.168.1.19:80 - true -``` diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 695afc2..0000000 --- a/settings.gradle +++ /dev/null @@ -1,16 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} -rootProject.name = "Word In Memory" -include ':app' diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..90ea92c --- /dev/null +++ b/styles.css @@ -0,0 +1,51 @@ +/* styles.css */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + } + + body, html { + height: 100%; + font-family: Arial, sans-serif; + } + + /* Royal blue background */ + body { + background-color: #002366; + display: flex; + justify-content: center; + align-items: center; + } + + /* Center container */ + .container { + display: flex; + align-items: center; + justify-content: center; + gap: 20px; /* Space between image and button */ + } + + /* Image styling */ + .smartphone-image { + max-width: 300px; + height: auto; + } + + /* Download button styling */ + .download-button { + display: inline-block; + padding: 15px 25px; + background-color: #FFA500; /* Orange color */ + color: white; + text-decoration: none; + font-weight: bold; + font-size: 1.2em; + border-radius: 8px; + transition: background-color 0.3s ease; + } + + .download-button:hover { + background-color: #cc8400; /* Darker orange on hover */ + } + \ No newline at end of file