diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..c4e4683 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +BCSD_Android_2025-1 \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..c224ad5 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..02a7102 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5791375..0e47b96 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,14 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + buildFeatures { + dataBinding = true + } + + kapt { + correctErrorTypes = true + } + buildTypes { release { isMinifyEnabled = false @@ -36,12 +45,18 @@ android { } dependencies { - implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.kotlinx.coroutines.android) + implementation(libs.androidx.room.runtime) + implementation("com.github.bumptech.glide:glide:4.16.0") + kapt ("com.github.bumptech.glide:compiler:4.16.0") + implementation(libs.androidx.room.ktx) + kapt(libs.androidx.room.compiler) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4c80941..47ea6b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + + diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt b/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt deleted file mode 100644 index 3ffa0eb..0000000 --- a/app/src/main/java/com/example/bcsd_android_2025_1/MainActivity.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.bcsd_android_2025_1 - -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) - setContentView(R.layout.activity_main) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/AddEditActivity.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/AddEditActivity.kt new file mode 100644 index 0000000..9bff1ed --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/AddEditActivity.kt @@ -0,0 +1,82 @@ +package com.example.bcsd_android_2025_1 + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.bumptech.glide.Glide +import com.example.bcsd_android_2025_1.databinding.ActivityAddEditBinding + +class AddEditActivity : AppCompatActivity() { + private lateinit var binding: ActivityAddEditBinding + private val viewModel: WordViewModel by viewModels() + + private var imageUri: String? = null + private var wordId: Int? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityAddEditBinding.inflate(layoutInflater) + setContentView(binding.root) + + val editWordText = intent.getStringExtra(MainActivity.wordTextKey) + val editMeaning = intent.getStringExtra(MainActivity.wordMeaningKey) + wordId = intent.getIntExtra(MainActivity.wordIdKey, -1).takeIf { it != -1 } + + val image = intent.getStringExtra(MainActivity.wordImageKey) + imageUri = image + + if (!imageUri.isNullOrEmpty()) { + Glide.with(this).load(imageUri).into(binding.addImageImageview) + } else { + binding.addImageImageview.setImageDrawable(null) + } + + binding.wordEdittext.setText(editWordText) + binding.meaningEdittext.setText(editMeaning) + + binding.addImageButton.setOnClickListener { + imagePickerLauncher.launch("image/*") + } + + binding.addButton.setOnClickListener { + val wordText = binding.wordEdittext.text.toString() + val meaning = binding.meaningEdittext.text.toString() + if (wordText.isNotBlank() && meaning.isNotBlank()) { + val word = WordListData(wordId ?: 0, wordText, meaning, imageUri) + if (wordId != null) { + viewModel.update(word) + binding.wordEdittext.setText("") + binding.meaningEdittext.setText("") + binding.addImageImageview.setImageDrawable(null) + imageUri = null + val resultIntent = Intent().apply { + putExtra(MainActivity.editedWordKey, word.word) + putExtra(MainActivity.editedMeaningKey, word.meaning) + putExtra(MainActivity.editedImageKey, word.imageUri) + } + setResult(Activity.RESULT_OK, resultIntent) + finish() + + } else { + viewModel.insert(word) + } + finish() + } + } + } + + private val imagePickerLauncher = + registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> + if (uri != null) { + imageUri = uri.toString() + Glide.with(this).load(uri).into(binding.addImageImageview) + } else { + imageUri = null + binding.addImageImageview.setImageDrawable(null) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/MainActivity.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/MainActivity.kt new file mode 100644 index 0000000..cf43ad2 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/MainActivity.kt @@ -0,0 +1,138 @@ +package com.example.bcsd_android_2025_1 + +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.example.bcsd_android_2025_1.databinding.ActivityMainBinding + + + +class MainActivity : AppCompatActivity() { + companion object{ + const val wordIdKey = "word_id" + const val wordTextKey = "word_text" + const val wordMeaningKey = "word_meaning" + const val wordImageKey = "word_imageUri" + const val editedWordKey = "edited_word" + const val editedMeaningKey = "edited_meaning" + const val editedImageKey = "edited_image" + } + + private lateinit var binding: ActivityMainBinding + private val viewModel: WordViewModel by viewModels() + private lateinit var editActivityLauncher: ActivityResultLauncher + + + private val adapter by lazy { + WordAdapter(onTopClick = { viewModel.setTopWord(it) }) + } + + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + startAddEditActivity() + } else { + Toast.makeText(this, R.string.need_image_permission, Toast.LENGTH_SHORT).show() + } + } + + private fun checkPermissionAndStart() { + val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + android.Manifest.permission.READ_MEDIA_IMAGES + } else { + android.Manifest.permission.READ_EXTERNAL_STORAGE + } + when { + ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED -> { + startAddEditActivity() + } + shouldShowRequestPermissionRationale(permission) -> { + requestPermissionLauncher.launch(permission) + } + else -> { + requestPermissionLauncher.launch(permission) + } + } + } + + private fun startAddEditActivity() { + val intent = Intent(this, AddEditActivity::class.java) + editActivityLauncher.launch(intent) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + editActivityLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + val editedWord = data?.getStringExtra(editedWordKey) ?: "" + val editedMeaning = data?.getStringExtra(editedMeaningKey) ?: "" + val editedImage = data?.getStringExtra(editedImageKey) + + binding.wordTextview.text = editedWord + binding.meaningTextview.text = editedMeaning + + if(!editedImage.isNullOrEmpty()){ + Glide.with(this).load(editedImage).into(binding.wordImageview) + }else{ + binding.wordImageview.setImageDrawable(null) + } + } + } + + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(this) + + binding.editButton.setOnClickListener { + viewModel.topWord.value?.let { + val intent = Intent(this, AddEditActivity::class.java).apply { + putExtra(wordIdKey, it.id) + putExtra(wordTextKey, it.word) + putExtra(wordMeaningKey, it.meaning) + putExtra(wordImageKey, it.imageUri) + } + editActivityLauncher.launch(intent) + } + } + + binding.deleteButton.setOnClickListener { + viewModel.topWord.value?.let { + viewModel.delete(it) + binding.wordTextview.text = "" + binding.meaningTextview.text = "" + binding.wordImageview.setImageDrawable(null) + } + } + + viewModel.allWords.observe(this) { + adapter.submitList(it) + } + + viewModel.topWord.observe(this) { + binding.wordTextview.text = it?.word ?: "" + binding.meaningTextview.text = it?.meaning ?: "" + if (!it?.imageUri.isNullOrEmpty()) { + Glide.with(this).load(it?.imageUri).into(binding.wordImageview) + } else { + binding.wordImageview.setImageDrawable(null) + } + } + + binding.floatingButton.setOnClickListener { + checkPermissionAndStart() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/WordAdapter.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/WordAdapter.kt new file mode 100644 index 0000000..8b0ef10 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/WordAdapter.kt @@ -0,0 +1,43 @@ +package com.example.bcsd_android_2025_1 + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.example.bcsd_android_2025_1.databinding.ItemWordBinding + +class WordAdapter ( + private val onTopClick: (WordListData) ->Unit +) : ListAdapter(DiffCallback()){ + + inner class WordViewHolder(private val binding: ItemWordBinding): RecyclerView.ViewHolder(binding.root) { + fun bind(word: WordListData) { + binding.word = word + if (!word.imageUri.isNullOrEmpty()) { + Glide.with(binding.recyclerviewImageview.context) + .load(word.imageUri) + .into(binding.recyclerviewImageview) + } else { + binding.recyclerviewImageview.setImageDrawable(null) + } + binding.root.setOnClickListener { onTopClick(word) } + binding.executePendingBindings() + } + } + + override fun onCreateViewHolder(parent:ViewGroup, viewType: Int):WordViewHolder{ + val binding = ItemWordBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return WordViewHolder(binding) + } + + override fun onBindViewHolder(holder: WordViewHolder, position:Int){ + holder.bind(getItem(position)) + } + + class DiffCallback: DiffUtil.ItemCallback(){ + override fun areItemsTheSame(oldItem:WordListData, newItem:WordListData):Boolean = oldItem.id ==newItem.id + override fun areContentsTheSame(oldItem: WordListData, newItem: WordListData): Boolean = oldItem == newItem + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/app/WordViewModel.kt b/app/src/main/java/com/example/bcsd_android_2025_1/app/WordViewModel.kt new file mode 100644 index 0000000..18d445c --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/app/WordViewModel.kt @@ -0,0 +1,24 @@ +package com.example.bcsd_android_2025_1 + +import android.app.Application +import androidx.lifecycle.* +import kotlinx.coroutines.launch + + +class WordViewModel(application: Application) : AndroidViewModel(application) { + private val repository: WordRepository + val allWords: LiveData> + private val _topWord = MutableLiveData() + val topWord: LiveData get() = _topWord + + init { + val wordDao = WordDatabase.getDatabase(application).wordDao() + repository = WordRepository(wordDao) + allWords = repository.allWords + } + + fun insert(word:WordListData) = viewModelScope.launch { repository.insert(word)} + fun update(word:WordListData) = viewModelScope.launch { repository.update(word)} + fun delete(word:WordListData) = viewModelScope.launch { repository.delete(word)} + fun setTopWord(word: WordListData) {_topWord.value = word} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/WordDao.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/WordDao.kt new file mode 100644 index 0000000..27eb462 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/WordDao.kt @@ -0,0 +1,20 @@ +package com.example.bcsd_android_2025_1 + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface WordDao { + @Query("SELECT * FROM word_table ORDER BY id DESC") + fun getAllwords(): LiveData> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(word: WordListData) + + @Update + suspend fun update(word: WordListData) + + @Delete + suspend fun delete(wordListData: WordListData) + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/WordDatabase.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/WordDatabase.kt new file mode 100644 index 0000000..6b1be58 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/WordDatabase.kt @@ -0,0 +1,28 @@ +package com.example.bcsd_android_2025_1 + +import android.content.Context +import androidx.room.Room +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database(entities = [WordListData::class], version = 2, exportSchema = false) +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" + ) + .fallbackToDestructiveMigration() + .build() + INSTANCE = instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/data/WordRepository.kt b/app/src/main/java/com/example/bcsd_android_2025_1/data/WordRepository.kt new file mode 100644 index 0000000..6235632 --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/data/WordRepository.kt @@ -0,0 +1,11 @@ +package com.example.bcsd_android_2025_1 + +import androidx.lifecycle.LiveData + +class WordRepository(private val wordDao: WordDao) { + val allWords: LiveData> = wordDao.getAllwords() + + suspend fun insert(word: WordListData) = wordDao.insert(word) + suspend fun update(word: WordListData) = wordDao.update(word) + suspend fun delete(word: WordListData) = wordDao.delete(word) +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bcsd_android_2025_1/domain/WordListData.kt b/app/src/main/java/com/example/bcsd_android_2025_1/domain/WordListData.kt new file mode 100644 index 0000000..d729faf --- /dev/null +++ b/app/src/main/java/com/example/bcsd_android_2025_1/domain/WordListData.kt @@ -0,0 +1,13 @@ +package com.example.bcsd_android_2025_1 + +import androidx.room.PrimaryKey +import androidx.room.Entity + +@Entity(tableName = "word_table") +data class WordListData ( + @PrimaryKey(autoGenerate = true) + val id: Int = 0, + var word: String, + var meaning: String, + val imageUri: String? = null +) \ No newline at end of file diff --git a/app/src/main/res/drawable/delete_icon.png b/app/src/main/res/drawable/delete_icon.png new file mode 100644 index 0000000..6f67376 Binary files /dev/null and b/app/src/main/res/drawable/delete_icon.png differ diff --git a/app/src/main/res/drawable/edit_icon.png b/app/src/main/res/drawable/edit_icon.png new file mode 100644 index 0000000..bb9a087 Binary files /dev/null and b/app/src/main/res/drawable/edit_icon.png differ diff --git a/app/src/main/res/layout/activity_add_edit.xml b/app/src/main/res/layout/activity_add_edit.xml new file mode 100644 index 0000000..2aef5bf --- /dev/null +++ b/app/src/main/res/layout/activity_add_edit.xml @@ -0,0 +1,70 @@ + + + + + + + + +