diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 148fdd2..bb44937 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2a4bbd8..c373d8d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + id("kotlin-kapt") } android { @@ -17,6 +18,11 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + buildFeatures { + viewBinding = true + dataBinding = true + } + buildTypes { release { isMinifyEnabled = false @@ -42,7 +48,15 @@ dependencies { implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.room.common.jvm) + implementation(libs.androidx.room.runtime.android) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.coil) + kapt(libs.androidx.room.compiler) } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 398b755..9e88c6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/android_25_2/MainActivity.kt b/app/src/main/java/com/example/android_25_2/MainActivity.kt deleted file mode 100644 index aed359b..0000000 --- a/app/src/main/java/com/example/android_25_2/MainActivity.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.android_25_2 - -import android.os.Bundle -import androidx.activity.enableEdgeToEdge -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContentView(R.layout.activity_main) - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) - insets - } - } -} \ No newline at end of file diff --git a/app/src/main/java/data/WordDao.kt b/app/src/main/java/data/WordDao.kt new file mode 100644 index 0000000..9461f50 --- /dev/null +++ b/app/src/main/java/data/WordDao.kt @@ -0,0 +1,23 @@ +package data + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update + +@Dao +interface WordDao { + @Insert + suspend fun insert(vararg word: WordEntity) + + @Update + suspend fun update(word: WordEntity) + + @Delete + suspend fun delete(word: WordEntity) + + @Query("SELECT * FROM word_table") + fun getAll(): LiveData> +} \ No newline at end of file diff --git a/app/src/main/java/data/WordDatabase.kt b/app/src/main/java/data/WordDatabase.kt new file mode 100644 index 0000000..1facacb --- /dev/null +++ b/app/src/main/java/data/WordDatabase.kt @@ -0,0 +1,28 @@ +package data + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase + +@Database(entities = [WordEntity::class], version = 1) +abstract class WordDatabase : RoomDatabase() { + abstract fun wordDao(): WordDao + + companion object { + @Volatile + private var INSTANCE: WordDatabase? = null + + fun getDatabase(context: Context): WordDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + WordDatabase::class.java, + "word_database" + ).build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/data/WordEntity.kt b/app/src/main/java/data/WordEntity.kt new file mode 100644 index 0000000..ba59b10 --- /dev/null +++ b/app/src/main/java/data/WordEntity.kt @@ -0,0 +1,13 @@ +package data + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity (tableName = "word_table") +data class WordEntity( + @PrimaryKey (autoGenerate = true) + val id: Int = 0, + val word: String, + val meaning: String, + val imagePath: String +) \ No newline at end of file diff --git a/app/src/main/java/presentation/MainActivity.kt b/app/src/main/java/presentation/MainActivity.kt new file mode 100644 index 0000000..61e3c6e --- /dev/null +++ b/app/src/main/java/presentation/MainActivity.kt @@ -0,0 +1,92 @@ +package presentation + +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import coil.load +import com.example.android_25_2.R +import com.example.android_25_2.databinding.ActivityMainBinding +import data.WordEntity + +class MainActivity : AppCompatActivity() { + + private lateinit var activityBinding: ActivityMainBinding + private lateinit var wordViewModel: WordViewModel + private lateinit var adapter: WordAdapter + companion object { + const val EXTRA_WORD_ID = "WORD_ID" + const val EXTRA_WORD = "WORD" + const val EXTRA_MEANING = "MEANING" + const val EXTRA_IMAGE_PATH = "IMAGE_PATH" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + activityBinding = ActivityMainBinding.inflate(layoutInflater) + setContentView(activityBinding.root) + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> + val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) + insets + } + + wordViewModel = ViewModelProvider(this)[WordViewModel::class.java] + adapter = WordAdapter() + activityBinding.recyclerView.adapter = adapter + activityBinding.recyclerView.layoutManager = LinearLayoutManager(this) + + activityBinding.buttonAdd.setOnClickListener { + startActivity(Intent(this, SecondActivity::class.java)) + } + + wordViewModel.allWords.observe(this) { words -> + adapter.setWords(words) + } + + adapter.setOnWordItemClickListener(object : WordAdapter.OnWordItemClickListener{ + override fun onWordItemClick(position: Int, word: WordEntity) { + showSelectedWord(word) + } + + override fun onEditButtonClick(word: WordEntity) { + val intent = Intent(this@MainActivity, SecondActivity::class.java) + intent.putExtra(EXTRA_WORD_ID, word.id) + intent.putExtra(EXTRA_WORD, word.word) + intent.putExtra(EXTRA_MEANING, word.meaning) + intent.putExtra(EXTRA_IMAGE_PATH, word.imagePath) + startActivity(intent) + } + + override fun onDeleteButtonClick(word: WordEntity) { + wordViewModel.delete(word) + } + }) + + activityBinding.buttonAdd.setOnClickListener{ + startActivity(Intent(this, SecondActivity::class.java)) + } + + wordViewModel.allWords.observe(this) {words -> + adapter.setWords(words) + } + } + private fun showSelectedWord(word: WordEntity) { + activityBinding.textViewSelectedWord.visibility = View.VISIBLE + activityBinding.textViewSelectedMeaning.visibility = View.VISIBLE + + activityBinding.textViewSelectedWord.text = word.word + activityBinding.textViewSelectedMeaning.text = word.meaning + + word.imagePath.let { + activityBinding.imageViewSelected.load(it) + activityBinding.imageViewSelected.visibility = View.VISIBLE + } + } +} \ No newline at end of file diff --git a/app/src/main/java/presentation/SecondActivity.kt b/app/src/main/java/presentation/SecondActivity.kt new file mode 100644 index 0000000..63800ec --- /dev/null +++ b/app/src/main/java/presentation/SecondActivity.kt @@ -0,0 +1,131 @@ +package presentation + +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.provider.Settings +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.ViewModelProvider +import coil.load +import com.example.android_25_2.databinding.ActivitySecondBinding +import androidx.core.net.toUri +import android.Manifest +import com.example.android_25_2.R +import data.WordEntity + +class SecondActivity : AppCompatActivity() { + + private lateinit var activityBinding: ActivitySecondBinding + private lateinit var wordViewModel: WordViewModel + private var wordId: Int? = null + private var selectedImageUri: Uri? = null + lateinit var pickImage: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + activityBinding = DataBindingUtil.setContentView(this, R.layout.activity_second) + + pickImage = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + uri?.let { + selectedImageUri = it + activityBinding.imageViewPreview.load(it) + } + } + + wordViewModel = ViewModelProvider(this)[WordViewModel::class.java] + + wordId = intent.getIntExtra(MainActivity.EXTRA_WORD_ID, -1) + val existingWord = intent.getStringExtra(MainActivity.EXTRA_WORD) + val existingMeaning = intent.getStringExtra(MainActivity.EXTRA_MEANING) + val existingImagePath = intent.getStringExtra(MainActivity.EXTRA_IMAGE_PATH) + + if (wordId != null) { + activityBinding.editTextWord.setText(existingWord) + activityBinding.editTextMeaning.setText(existingMeaning) + existingImagePath?.let { + activityBinding.imageViewPreview.load(it) + } + } + + activityBinding.buttonSelectImage.setOnClickListener { + permissionCheck() + } + + activityBinding.buttonSave.setOnClickListener { + val word = activityBinding.editTextWord.text.toString() + val meaning = activityBinding.editTextMeaning.text.toString() + + if (word.isEmpty() || meaning.isEmpty()) { + Toast.makeText(this, R.string.empty_message, Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + + val imagePath: String = when { + selectedImageUri != null -> selectedImageUri.toString() + existingImagePath != null -> existingImagePath + else -> "" + } + + if (wordId != null && wordId != -1) { + val wordEntity = WordEntity( + id = wordId!!, + word = word, + meaning = meaning, + imagePath = imagePath + ) + wordViewModel.update(wordEntity) + } else { + val wordEntity = WordEntity( + word = word, + meaning = meaning, + imagePath = imagePath + ) + wordViewModel.insert(wordEntity) + } + finish() + } + } + private fun permissionCheck() { + val mediaPermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + + if (ContextCompat.checkSelfPermission(this, mediaPermission) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions( + this, arrayOf(mediaPermission), 1000 + ) + + if (!ActivityCompat.shouldShowRequestPermissionRationale(this, mediaPermission)) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData("package:${packageName}".toUri()) + startActivity(intent) + } + } else { + pickImage.launch("image/*") + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == 1000) { + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + pickImage.launch("image/*") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/presentation/WordAdapter.kt b/app/src/main/java/presentation/WordAdapter.kt new file mode 100644 index 0000000..ed5252e --- /dev/null +++ b/app/src/main/java/presentation/WordAdapter.kt @@ -0,0 +1,79 @@ +package presentation + +import android.view.ViewGroup +import android.view.LayoutInflater +import androidx.recyclerview.widget.RecyclerView +import coil.load +import com.example.android_25_2.databinding.ItemBinding +import data.WordEntity + +class WordAdapter: RecyclerView.Adapter(){ + + private var words = emptyList() + + interface OnWordItemClickListener { + fun onWordItemClick(position: Int, word: WordEntity) + fun onEditButtonClick(word: WordEntity) + fun onDeleteButtonClick(word: WordEntity) + } + + private var listener: OnWordItemClickListener? = null + fun setOnWordItemClickListener(listener: OnWordItemClickListener) { + this.listener = listener + } + + inner class WordViewHolder(val binding: ItemBinding) : + RecyclerView.ViewHolder(binding.root) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder { + val binding = ItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return WordViewHolder(binding) + } + + override fun onBindViewHolder(holder: WordViewHolder, position: Int) { + val word = words[position] + holder.binding.word = word + holder.binding.executePendingBindings() + + word.imagePath?.let { path -> + holder.binding.imageView.load(path) + } + + holder.binding.root.setOnClickListener { + listener?.onWordItemClick(holder.adapterPosition, word) + } + + holder.binding.buttonEdit.setOnClickListener { + listener?.onEditButtonClick(word) + } + + holder.binding.buttonDelete.setOnClickListener { + listener?.onDeleteButtonClick(word) + } + } + + override fun getItemCount() = words.size + + fun setWords(newWords: List) { + this.words = newWords + notifyDataSetChanged() + } + + fun getWord(position: Int): WordEntity { + return words[position] + } + + fun moveToTop(position: Int) { + val word = words[position] + val currentList = words.toMutableList() + currentList.removeAt(position) + currentList.add(0, word) + words = currentList + + notifyItemMoved(position, 0) + } +} \ No newline at end of file diff --git a/app/src/main/java/presentation/WordViewModel.kt b/app/src/main/java/presentation/WordViewModel.kt new file mode 100644 index 0000000..ffdb607 --- /dev/null +++ b/app/src/main/java/presentation/WordViewModel.kt @@ -0,0 +1,34 @@ +package presentation + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.viewModelScope +import data.WordDao +import data.WordDatabase +import data.WordEntity +import kotlinx.coroutines.launch + +class WordViewModel(application: Application) : AndroidViewModel(application) { + + private val wordDao: WordDao + val allWords: LiveData> + + init { + val database = WordDatabase.getDatabase(application) + wordDao = database.wordDao() + allWords = wordDao.getAll() + } + + fun insert(word: WordEntity) = viewModelScope.launch { + wordDao.insert(word) + } + + fun update(word: WordEntity) = viewModelScope.launch { + wordDao.update(word) + } + + fun delete(word: WordEntity) = viewModelScope.launch { + wordDao.delete(word) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/id_add.xml b/app/src/main/res/drawable/id_add.xml new file mode 100644 index 0000000..9f83b8f --- /dev/null +++ b/app/src/main/res/drawable/id_add.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 9affce0..2ae128d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,10 +1,61 @@ - + xmlns:tools="http://schemas.android.com/tools"> - \ No newline at end of file + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_second.xml b/app/src/main/res/layout/activity_second.xml new file mode 100644 index 0000000..540fdc9 --- /dev/null +++ b/app/src/main/res/layout/activity_second.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + +