diff --git a/app/build.gradle b/app/build.gradle index 75fbc7c..79975f1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -15,7 +15,11 @@ android { targetSdk 33 versionCode 1 versionName "1.0" - + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas".toString()) + } + } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -36,22 +40,30 @@ android { } buildFeatures { - viewBinding = true + dataBinding true } } dependencies { + implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'com.google.android.material:material:1.8.0' + implementation "androidx.fragment:fragment-ktx:1.5.5" implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation "androidx.cardview:cardview:1.0.0" + + // Room ORM def room_version = "2.5.0" - def kotlin_version = "1.7.21" - def nav_version = "2.5.3" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" + + // Coroutines KTX + def lifecycle_version = "2.5.1" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 92bed61..e25a657 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + tools:targetApi="33"> + android:name=".ui.activity.EntryListActivity" + android:exported="true" + android:launchMode="singleTop"> - - - + + \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/MainActivity.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/MainActivity.kt deleted file mode 100644 index 16ad1dd..0000000 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/MainActivity.kt +++ /dev/null @@ -1,17 +0,0 @@ -package fr.uca.iut.urbandictionarylight - -import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity -import fr.uca.iut.urbandictionarylight.data.UrbanDictionaryDatabase -import fr.uca.iut.urbandictionarylight.databinding.ActivityMainBinding - -class MainActivity : AppCompatActivity() { - - lateinit var binding: ActivityMainBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/UrbanDictionaryLight.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/UrbanDictionaryLight.kt new file mode 100644 index 0000000..cba32fe --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/UrbanDictionaryLight.kt @@ -0,0 +1,11 @@ +package fr.uca.iut.urbandictionarylight + +import android.app.Application +import fr.uca.iut.urbandictionarylight.data.UrbanDictionaryDatabase + +class UrbanDictionaryLight : Application() { + override fun onCreate() { + super.onCreate() + UrbanDictionaryDatabase.initialize(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/DefinitionDao.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/DefinitionDao.kt deleted file mode 100644 index d655bdc..0000000 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/DefinitionDao.kt +++ /dev/null @@ -1,22 +0,0 @@ -package fr.uca.iut.urbandictionarylight.data - -import androidx.room.* -import fr.uca.iut.urbandictionarylight.model.Definition - -@Dao -interface DefinitionDao { - @Query("SELECT * FROM definitions") - fun queryAll(): Set - - @Query("SELECT * FROM entries WHERE id == :id") - fun query(id: Int): Definition? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(entry: Definition) - - @Update - fun update(entry: Definition) - - @Delete - fun delete(entry: Definition) -} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/EntryDao.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/EntryDao.kt deleted file mode 100644 index e134831..0000000 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/EntryDao.kt +++ /dev/null @@ -1,23 +0,0 @@ -package fr.uca.iut.urbandictionarylight.data - -import androidx.room.* -import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions - - -@Dao -interface EntryDao { - @Query("SELECT * FROM entries") - fun queryAll(): List - - @Query("SELECT * FROM entries WHERE id == :id") - fun query(id: Int): EntryWithDefinitions? - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(entry: EntryWithDefinitions) - - @Update - fun update(entry: EntryWithDefinitions) - - @Delete - fun delete(entry: EntryWithDefinitions) -} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/UrbanDictionaryDatabase.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/UrbanDictionaryDatabase.kt index 5987386..48ce3a2 100644 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/UrbanDictionaryDatabase.kt +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/UrbanDictionaryDatabase.kt @@ -4,9 +4,9 @@ import android.app.Application import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import fr.uca.iut.urbandictionarylight.data.entry.EntryDao import fr.uca.iut.urbandictionarylight.model.Definition import fr.uca.iut.urbandictionarylight.model.Entry -import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions private const val DB_FILE = "u_d_light.db" @@ -14,7 +14,7 @@ private const val DB_FILE = "u_d_light.db" abstract class UrbanDictionaryDatabase : RoomDatabase() { abstract fun entryDao(): EntryDao - abstract fun definitionDao(): DefinitionDao + abstract fun definitionDao(): fr.uca.iut.urbandictionarylight.data.definition.DefinitionDao companion object { private lateinit var application: Application @@ -32,65 +32,19 @@ abstract class UrbanDictionaryDatabase : RoomDatabase() { UrbanDictionaryDatabase::class.java, DB_FILE ) - .allowMainThreadQueries() .build() - - instance?.entryDao()?.let { - if (it.queryAll().isEmpty()) emptyDatabaseStub(it) - } } } return instance!! } else - throw RuntimeException("the database must be initialized first") + throw RuntimeException("must initialize DB first") } - //???? @Synchronized fun initialize(app: Application) { if (Companion::application.isInitialized) - throw RuntimeException("the database must not be initialized twice") - + throw RuntimeException("can't initialize DB twice") application = app } - - - private fun emptyDatabaseStub( - entryDao: EntryDao, - definitionDao: DefinitionDao - ): () -> Unit = { - - val def1 = Definition("bleep", "bleep blap bloop", 1) - val def2 = Definition("blarp", "bleeeep blap bloop", 7) - val def3 = Definition("blep", "bleep blap blooop", 4) - val def4 = Definition("blupo", "bleep blaap bloop", 56) - - with(definitionDao) { - insert(def1) - insert(def2) - insert(def3) - insert(def4) - } - with(entryDao) { - insert( - EntryWithDefinitions( - Entry("shrimping"), - listOf(def1, def4) - ) - ) - insert( - EntryWithDefinitions( - Entry("blunderkegel"), - listOf(def2) - ) - ) - insert( - EntryWithDefinitions( - Entry("something horrible and dumb"), - listOf(def3) - ) - ) - } - } } } diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/definition/DefinitionDao.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/definition/DefinitionDao.kt new file mode 100644 index 0000000..3150267 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/definition/DefinitionDao.kt @@ -0,0 +1,23 @@ +package fr.uca.iut.urbandictionarylight.data.definition + +import androidx.lifecycle.LiveData +import androidx.room.* +import fr.uca.iut.urbandictionarylight.model.Definition + +@Dao +interface DefinitionDao { + @Query("SELECT * FROM definitions") + fun queryAll(): LiveData> + + @Query("SELECT * FROM entries WHERE id = :id") + fun query(id: Long): LiveData + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entry: Definition) + + @Update(onConflict = OnConflictStrategy.REPLACE) + suspend fun update(entry: Definition) + + @Delete + suspend fun delete(entry: Definition) +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/definition/DefinitionRepository.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/definition/DefinitionRepository.kt new file mode 100644 index 0000000..28140bc --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/definition/DefinitionRepository.kt @@ -0,0 +1,38 @@ +package fr.uca.iut.urbandictionarylight.data.definition + +import android.util.Log +import androidx.lifecycle.LiveData +import fr.uca.iut.urbandictionarylight.model.Definition +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +private const val TAG = "DefinitionRepository" + + +class DefinitionRepository(private val definitionDao: DefinitionDao) { + + fun getAll(): LiveData> { + Log.i(TAG, "GET ALL DEFINITIONS") + return definitionDao.queryAll() + } + + fun findById(definitionId: Long): LiveData { + Log.i(TAG, "GET DEFINITION") + return definitionDao.query(definitionId) + } + + suspend fun insert(definition: Definition) = withContext(Dispatchers.IO) { + Log.i(TAG, "POST DEFINITION") + definitionDao.insert(definition) + } + + suspend fun delete(definition: Definition) = withContext(Dispatchers.IO) { + Log.i(TAG, "DELETE DEFINITION") + definitionDao.delete(definition) + } + + suspend fun update(definition: Definition) = withContext(Dispatchers.IO) { + Log.i(TAG, "PUT DEFINITION") + definitionDao.update(definition) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/entry/EntryDao.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/entry/EntryDao.kt new file mode 100644 index 0000000..faef797 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/entry/EntryDao.kt @@ -0,0 +1,24 @@ +package fr.uca.iut.urbandictionarylight.data.entry + +import androidx.lifecycle.LiveData +import androidx.room.* +import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions + + +@Dao +interface EntryDao { + @Query("SELECT * FROM entries") + fun queryAll(): LiveData> + + @Query("SELECT * FROM entries WHERE id = :id") + fun query(id: Long): LiveData + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(entry: EntryWithDefinitions) + + @Update + suspend fun update(entry: EntryWithDefinitions) + + @Delete + suspend fun delete(entry: EntryWithDefinitions) +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/data/entry/EntryRepository.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/entry/EntryRepository.kt new file mode 100644 index 0000000..1b5bb47 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/data/entry/EntryRepository.kt @@ -0,0 +1,37 @@ +package fr.uca.iut.urbandictionarylight.data.entry + +import android.util.Log +import androidx.lifecycle.LiveData +import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +private const val TAG = "EntryRepository" + +class EntryRepository(private val entryDao: EntryDao) { + + fun getAll(): LiveData> { + Log.i(TAG, "GET ALL ENTRIES") + return entryDao.queryAll() + } + + fun findById(entryId: Long): LiveData { + Log.i(TAG, "GET ENTRY") + return entryDao.query(entryId) + } + + suspend fun insert(entry: EntryWithDefinitions) = withContext(Dispatchers.IO) { + Log.i(TAG, "POST ENTRY") + entryDao.insert(entry) + } + + suspend fun delete(entry: EntryWithDefinitions) = withContext(Dispatchers.IO) { + Log.i(TAG, "DELETE ENTRY") + entryDao.delete(entry) + } + + suspend fun update(entry: EntryWithDefinitions) = withContext(Dispatchers.IO) { + Log.i(TAG, "PUT ENTRY") + entryDao.update(entry) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/model/Dictionary.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/model/Dictionary.kt deleted file mode 100644 index 5f72e47..0000000 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/model/Dictionary.kt +++ /dev/null @@ -1,45 +0,0 @@ -// UHM THIS WILL PROBABLY BE THE REPOSITORY ITSELF - -/* -package fr.uca.iut.urbandictionarylight.model - -@Entity -class Dictionary { - private val entries: MutableSet = mutableSetOf() - - fun createEntry(entry: Entry) { - if (entry.phrase.isNotBlank() && entry.readAllDefinitions().isNotEmpty()) { - entries.add(entry) - } - } - - fun readEntry(id: Int): Entry { - // TODO implement once ORM is in place - return Entry(phrase = "womp womp -- WIP").also { e -> e.createDefinition(e.readDefinition(-1)) } - } - - fun readAllEntries() = entries.toSet() - - fun updateEntry(entry: Entry) { - // TODO implement once ORM is in place -// removeEntry(entry.id) - createEntry(entry) - } - - // TODO throw this away soon - fun updateEntry(old: Entry, new: Entry) { - deleteEntry(old) - createEntry(new) - } - - fun deleteEntry(entry: Entry) { - entries.remove(entry) - } - - fun deleteEntry(id: Int) { - // TODO implement once ORM is in place -// entries.removeIf { e -> id == e.id } - } - - // TODO ? redefine equals and hashset ? -}*/ diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/model/Entry.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/model/Entry.kt index 929debf..c4d2d1d 100644 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/model/Entry.kt +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/model/Entry.kt @@ -7,6 +7,6 @@ const val NEW_ENTRY_ID = 0L @Entity(tableName = "entries") data class Entry( - val phrase: String, + val phrase: String = "", @PrimaryKey(autoGenerate = true) val id: Long = NEW_ENTRY_ID ) \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/model/EntryWithDefinitions.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/model/EntryWithDefinitions.kt index 2611045..bf48de9 100644 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/model/EntryWithDefinitions.kt +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/model/EntryWithDefinitions.kt @@ -4,9 +4,9 @@ import androidx.room.Embedded import androidx.room.Relation data class EntryWithDefinitions( - @Embedded val entry: Entry, + @Embedded val entry: Entry = Entry(), @Relation( parentColumn = "id", entityColumn = "entryId" - ) val definitions: List + ) val definitions: List = listOf() ) \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryActivity.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryActivity.kt new file mode 100644 index 0000000..4436f09 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryActivity.kt @@ -0,0 +1,35 @@ +package fr.uca.iut.urbandictionarylight.ui.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import fr.uca.iut.urbandictionarylight.R +import fr.uca.iut.urbandictionarylight.model.NEW_ENTRY_ID +import fr.uca.iut.urbandictionarylight.ui.fragment.EntryFragment + +class EntryActivity : SimpleFragmentActivity(), EntryFragment.OnInteractionListener { + companion object { + private const val EXTRA_ENTRY_ID = + "fr.uca.iut.urbandictionarylight.ui.activity.extra_entry_id" + + fun getIntent(context: Context, entryId: Long) = + Intent(context, EntryActivity::class.java).apply { + putExtra(EXTRA_ENTRY_ID, entryId) + } + } + + private var entryId = NEW_ENTRY_ID + + + override fun onCreate(savedInstanceState: Bundle?) { + entryId = intent.getLongExtra(EXTRA_ENTRY_ID, NEW_ENTRY_ID) + super.onCreate(savedInstanceState) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + + override fun createFragment() = EntryFragment.newInstance(entryId) + override fun getLayoutResId() = R.layout.toolbar_activity + override fun onEntrySaved() = finish() + override fun onEntryDeleted() = finish() +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryListActivity.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryListActivity.kt new file mode 100644 index 0000000..5622780 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryListActivity.kt @@ -0,0 +1,70 @@ +package fr.uca.iut.urbandictionarylight.ui.activity + +import android.os.Bundle +import android.widget.FrameLayout +import fr.uca.iut.urbandictionarylight.R +import fr.uca.iut.urbandictionarylight.model.NEW_ENTRY_ID +import fr.uca.iut.urbandictionarylight.ui.fragment.EntryFragment +import fr.uca.iut.urbandictionarylight.ui.fragment.EntryListFragment + +class EntryListActivity : SimpleFragmentActivity(), + EntryListFragment.OnInteractionListener, EntryFragment.OnInteractionListener { + + private var isTwoPane: Boolean = false + private lateinit var masterFragment: EntryListFragment + + override fun createFragment() = EntryListFragment().also { masterFragment = it } + override fun getLayoutResId() = R.layout.toolbar_md_activity + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + isTwoPane = findViewById(R.id.container_fragment_detail) != null + if (savedInstanceState != null) + masterFragment = supportFragmentManager.findFragmentById(R.id.container_fragment) as EntryListFragment + + if (!isTwoPane) { + removeDisplayedFragment() + } + } + + + override fun onEntrySelected(entryId: Long) { + if (isTwoPane) { + supportFragmentManager.beginTransaction() + .replace(R.id.container_fragment_detail, EntryFragment.newInstance(entryId)) + .commit() + } else { + startActivity(EntryPagerActivity.getIntent(this, entryId)) + } + } + + + override fun onAddNewEntry() = startActivity(EntryActivity.getIntent(this, NEW_ENTRY_ID)) + + + override fun onEntrySaved() { } + + + private fun removeDisplayedFragment() { + supportFragmentManager.findFragmentById(R.id.container_fragment_detail)?.let { + supportFragmentManager.beginTransaction().remove(it).commit() + } + } + + + override fun onEntryDeleted() { + if (isTwoPane) { + removeDisplayedFragment() + } else + finish() + } + + + override fun onEntrySwiped() { + if (isTwoPane) { + removeDisplayedFragment() + } + } +} diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryPagerActivity.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryPagerActivity.kt new file mode 100644 index 0000000..c259ceb --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/EntryPagerActivity.kt @@ -0,0 +1,73 @@ +package fr.uca.iut.urbandictionarylight.ui.activity + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.LinearLayout +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.viewpager2.widget.ViewPager2 +import fr.uca.iut.urbandictionarylight.R +import fr.uca.iut.urbandictionarylight.model.NEW_ENTRY_ID +import fr.uca.iut.urbandictionarylight.ui.fragment.EntryFragment +import fr.uca.iut.urbandictionarylight.ui.utils.EntryPagerAdapter +import fr.uca.iut.urbandictionarylight.ui.viewmodel.EntryPagerViewModel + + +class EntryPagerActivity : AppCompatActivity(), EntryFragment.OnInteractionListener { + + companion object { + private const val EXTRA_ENTRY_ID = + "fr.uca.iut.urbandictionarylight.ui.activity.extra_entry_id" + + fun getIntent(context: Context, entryId: Long) = + Intent(context, EntryPagerActivity::class.java).apply { + putExtra(EXTRA_ENTRY_ID, entryId) + } + } + + private val pagerAdapter = EntryPagerAdapter(this) + private val entryPagerVM by viewModels() + private lateinit var viewPager: ViewPager2 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_pager) + + setSupportActionBar(findViewById(R.id.toolbar_activity)) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + viewPager = ViewPager2(this) + viewPager.id = R.id.view_pager + findViewById(R.id.pager_layout).addView(viewPager) + + viewPager.adapter = pagerAdapter + entryPagerVM.currentEntryId = + savedInstanceState?.getLong(EXTRA_ENTRY_ID) ?: intent.getLongExtra( + EXTRA_ENTRY_ID, + NEW_ENTRY_ID + ) + + entryPagerVM.entryList.observe(this) { + pagerAdapter.submitList(it) + var position = pagerAdapter.positionFromId(entryPagerVM.currentEntryId) + if (position == -1) position = 0 + viewPager.currentItem = position + } + + viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + entryPagerVM.currentEntryId = pagerAdapter.entryIdAt(position) + } + }) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putLong(EXTRA_ENTRY_ID, entryPagerVM.currentEntryId) + } + + override fun onEntrySaved() = finish() + override fun onEntryDeleted() = finish() +} + diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/SimpleFragmentActivity.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/SimpleFragmentActivity.kt new file mode 100644 index 0000000..7f97090 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/activity/SimpleFragmentActivity.kt @@ -0,0 +1,29 @@ +package fr.uca.iut.urbandictionarylight.ui.activity + +import android.os.Bundle +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import fr.uca.iut.urbandictionarylight.R + +abstract class SimpleFragmentActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(getLayoutResId()) + + setSupportActionBar(findViewById(R.id.toolbar_activity)) + + if (supportFragmentManager.findFragmentById(R.id.container_fragment) == null) { + supportFragmentManager.beginTransaction() + .add(R.id.container_fragment, createFragment()) + .commit() + } + } + + protected abstract fun createFragment(): Fragment + + @LayoutRes + protected abstract fun getLayoutResId(): Int +} diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/fragment/EntryFragment.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/fragment/EntryFragment.kt new file mode 100644 index 0000000..b6b51d2 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/fragment/EntryFragment.kt @@ -0,0 +1,138 @@ +package fr.uca.iut.urbandictionarylight.ui.fragment + +import android.content.Context +import android.os.Bundle +import android.view.* +import androidx.appcompat.app.AlertDialog +import androidx.core.os.bundleOf +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.get +import androidx.lifecycle.viewmodel.viewModelFactory +import fr.uca.iut.urbandictionarylight.R +import fr.uca.iut.urbandictionarylight.databinding.FragmentEntryBinding +import fr.uca.iut.urbandictionarylight.model.NEW_ENTRY_ID +import fr.uca.iut.urbandictionarylight.ui.viewmodel.EntryViewModel + +class EntryFragment : Fragment() { + + companion object { + private const val EXTRA_ENTRY_ID = + "fr.uca.iut.urbandictionarylight.ui.activity.extra_entry_id" + + fun newInstance(entryId: Long) = EntryFragment().apply { + arguments = bundleOf(EXTRA_ENTRY_ID to entryId) + } + } + + private var entryId: Long = NEW_ENTRY_ID + private lateinit var entryVM: EntryViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + entryId = savedInstanceState?.getLong(EXTRA_ENTRY_ID) ?: arguments?.getLong(EXTRA_ENTRY_ID) + ?: NEW_ENTRY_ID + + + if (entryId == NEW_ENTRY_ID) { + requireActivity().setTitle(R.string.add_entry_lbl) + } + + entryVM = ViewModelProvider(this, viewModelFactory { EntryViewModel(entryId) }).get() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putLong(EXTRA_ENTRY_ID, entryId) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = FragmentEntryBinding.inflate(inflater) + binding.entryVM = entryVM + binding.lifecycleOwner = viewLifecycleOwner + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupMenu() + } + + private fun setupMenu() { + requireActivity().addMenuProvider(object : MenuProvider { + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + if (entryId == NEW_ENTRY_ID) { + menu.findItem(R.id.action_delete)?.isVisible = false + } + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_entry, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_save -> { + saveEntry() + true + } + R.id.action_delete -> { + deleteEntry() + true + } + else -> false + } + } + + }, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + + private fun saveEntry() { + if (entryVM.saveEntry() == true) { + listener?.onEntrySaved() + } else { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.create_entry_error_dialog_title) + .setMessage(R.string.create_entry_error_message) + .setNeutralButton(android.R.string.ok, null) + .show() + return + } + } + + private fun deleteEntry() { + if (entryId != NEW_ENTRY_ID) { + entryVM.deleteEntry() + listener?.onEntryDeleted() + } + } + + interface OnInteractionListener { + fun onEntrySaved() + fun onEntryDeleted() + } + + private var listener: OnInteractionListener? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnInteractionListener) { + listener = context + } else { + throw RuntimeException("$context must implement OnInteractionListener") + } + } + + override fun onDetach() { + super.onDetach() + listener = null + } +} diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/fragment/EntryListFragment.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/fragment/EntryListFragment.kt new file mode 100644 index 0000000..6d87898 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/fragment/EntryListFragment.kt @@ -0,0 +1,129 @@ +package fr.uca.iut.urbandictionarylight.ui.fragment + +import android.content.Context +import android.os.Bundle +import android.view.* +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import fr.uca.iut.urbandictionarylight.R +import fr.uca.iut.urbandictionarylight.databinding.FragmentListEntryBinding +import fr.uca.iut.urbandictionarylight.ui.utils.EntryRecyclerViewAdapter +import fr.uca.iut.urbandictionarylight.ui.viewmodel.EntryListViewModel + + +class EntryListFragment : Fragment(), EntryRecyclerViewAdapter.Callbacks { + + private val entryListAdapter = EntryRecyclerViewAdapter(this) + private val entryListVM by viewModels() + + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val binding = FragmentListEntryBinding.inflate(inflater) + binding.entryListVM = entryListVM + binding.lifecycleOwner = viewLifecycleOwner + with(binding.recyclerView) { + adapter = entryListAdapter + ItemTouchHelper(EntryListItemTouchHelper()).attachToRecyclerView(this) + } + binding.addEntryBtn.setOnClickListener { addNewEntry() } + return binding.root + } + + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupMenu() + + entryListVM.entryList.observe(viewLifecycleOwner) { + entryListAdapter.submitList(it) + } + } + + + private fun setupMenu() { + requireActivity().addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_list_entry, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.menu_item_new_entry -> { + addNewEntry() + true + } + else -> false + } + } + }, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + + + private fun addNewEntry() { + listener?.onAddNewEntry() + } + + + override fun onEntrySelected(entryId: Long) { + listener?.onEntrySelected(entryId) + } + + + private inner class EntryListItemTouchHelper : ItemTouchHelper.Callback() { + override fun isLongPressDragEnabled() = false + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) = + makeMovementFlags( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.START or ItemTouchHelper.END + ) + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ) = false + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + (viewHolder as EntryRecyclerViewAdapter.EntryViewHolder).entry?.also { + entryListVM.delete(it) + listener?.onEntrySwiped() + } + } + } + + + interface OnInteractionListener { + fun onEntrySelected(entryId: Long) + fun onAddNewEntry() + fun onEntrySwiped() + } + + private var listener: OnInteractionListener? = null + + + override fun onAttach(context: Context) { + super.onAttach(context) + if (context is OnInteractionListener) { + listener = context + } else { + throw RuntimeException("$context must implement OnInteractionListener") + } + } + + + override fun onDetach() { + super.onDetach() + listener = null + } +} diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/EntryPagerAdapter.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/EntryPagerAdapter.kt new file mode 100644 index 0000000..f3f671b --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/EntryPagerAdapter.kt @@ -0,0 +1,27 @@ +package fr.uca.iut.urbandictionarylight.ui.utils + +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions +import fr.uca.iut.urbandictionarylight.model.NEW_ENTRY_ID +import fr.uca.iut.urbandictionarylight.ui.fragment.EntryFragment + +class EntryPagerAdapter(fragmentActivity: FragmentActivity) : + FragmentStateAdapter(fragmentActivity) { + private var entryList = listOf() + + override fun getItemCount() = entryList.size + + override fun createFragment(position: Int) = + EntryFragment.newInstance(entryList[position].entry.id) + + fun positionFromId(entryId: Long) = entryList.indexOfFirst { it.entry.id == entryId } + + fun entryIdAt(position: Int) = + if (entryList.isEmpty()) NEW_ENTRY_ID else entryList[position].entry.id + + fun submitList(entryList: List) { + this.entryList = entryList + notifyDataSetChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/EntryRecyclerViewAdapter.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/EntryRecyclerViewAdapter.kt new file mode 100644 index 0000000..fb1aeb6 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/EntryRecyclerViewAdapter.kt @@ -0,0 +1,56 @@ +package fr.uca.iut.urbandictionarylight.ui.utils + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import fr.uca.iut.urbandictionarylight.databinding.ItemListEntryBinding +import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions + +class EntryRecyclerViewAdapter(private val listener: Callbacks) : + ListAdapter( + DiffUtilEntryCallback + ) { + + private object DiffUtilEntryCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: EntryWithDefinitions, newItem: EntryWithDefinitions) = + oldItem.entry.id == newItem.entry.id + + override fun areContentsTheSame( + oldItem: EntryWithDefinitions, + newItem: EntryWithDefinitions + ) = oldItem == newItem + } + + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + EntryViewHolder( + ItemListEntryBinding.inflate(LayoutInflater.from(parent.context)), listener + ) + + + override fun onBindViewHolder(holder: EntryViewHolder, position: Int) = + holder.bind(getItem(position)) + + + class EntryViewHolder(private val binding: ItemListEntryBinding, listener: Callbacks) : + RecyclerView.ViewHolder(binding.root) { + + val entry: EntryWithDefinitions? get() = binding.entryWithDefinitions + + init { + itemView.setOnClickListener { entry?.let { listener.onEntrySelected(it.entry.id) } } + } + + fun bind(entryWithDefinitions: EntryWithDefinitions) { + binding.entryWithDefinitions = entryWithDefinitions + binding.executePendingBindings() + } + + } + + interface Callbacks { + fun onEntrySelected(entryId: Long) + } +} diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/Utils.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/Utils.kt new file mode 100644 index 0000000..f8202dc --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/utils/Utils.kt @@ -0,0 +1,10 @@ +package fr.uca.iut.urbandictionarylight.ui.utils + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + + +@Suppress("UNCHECKED_CAST") +inline fun viewModelFactory(crossinline f: () -> VM) = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T = f() as T +} diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryListViewModel.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryListViewModel.kt new file mode 100644 index 0000000..4b12a59 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryListViewModel.kt @@ -0,0 +1,18 @@ +package fr.uca.iut.urbandictionarylight.ui.viewmodel + +import androidx.lifecycle.Transformations +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import fr.uca.iut.urbandictionarylight.data.UrbanDictionaryDatabase +import fr.uca.iut.urbandictionarylight.data.entry.EntryRepository +import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions +import kotlinx.coroutines.launch + +class EntryListViewModel : ViewModel() { + private val entryRepo = EntryRepository(UrbanDictionaryDatabase.getInstance().entryDao()) + + val entryList = entryRepo.getAll() + + fun delete(entry: EntryWithDefinitions) = viewModelScope.launch { entryRepo.delete(entry) } + +} diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryPagerViewModel.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryPagerViewModel.kt new file mode 100644 index 0000000..2f12347 --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryPagerViewModel.kt @@ -0,0 +1,12 @@ +package fr.uca.iut.urbandictionarylight.ui.viewmodel + +import androidx.lifecycle.ViewModel +import fr.uca.iut.urbandictionarylight.data.UrbanDictionaryDatabase +import fr.uca.iut.urbandictionarylight.data.entry.EntryRepository +import fr.uca.iut.urbandictionarylight.model.NEW_ENTRY_ID + +class EntryPagerViewModel : ViewModel() { + private val entryRepo = EntryRepository(UrbanDictionaryDatabase.getInstance().entryDao()) + val entryList = entryRepo.getAll() + var currentEntryId = NEW_ENTRY_ID +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryViewModel.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryViewModel.kt new file mode 100644 index 0000000..a10e11c --- /dev/null +++ b/app/src/main/java/fr/uca/iut/urbandictionarylight/ui/viewmodel/EntryViewModel.kt @@ -0,0 +1,35 @@ +package fr.uca.iut.urbandictionarylight.ui.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import fr.uca.iut.urbandictionarylight.data.UrbanDictionaryDatabase +import fr.uca.iut.urbandictionarylight.data.entry.EntryRepository +import fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions +import fr.uca.iut.urbandictionarylight.model.NEW_ENTRY_ID +import kotlinx.coroutines.launch + + +class EntryViewModel(entryId: Long) : ViewModel() { + private val entryRepo = EntryRepository(UrbanDictionaryDatabase.getInstance().entryDao()) + + val entry = + if (entryId == NEW_ENTRY_ID) MutableLiveData(EntryWithDefinitions()) else entryRepo.findById( + entryId + ) + + fun saveEntry() = entry.value?.let { + if (it.entry.phrase.isBlank() || it.definitions.isEmpty()) + false + else { + viewModelScope.launch { + if (it.entry.id == NEW_ENTRY_ID) entryRepo.insert(it) else entryRepo.update(it) + } + true + } + } + + fun deleteEntry() = viewModelScope.launch { + entry.value?.let { if (it.entry.id != NEW_ENTRY_ID) entryRepo.delete(it) } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/uca/iut/urbandictionarylight/utils/Sorting.kt b/app/src/main/java/fr/uca/iut/urbandictionarylight/utils/Sorting.kt deleted file mode 100644 index 4699a98..0000000 --- a/app/src/main/java/fr/uca/iut/urbandictionarylight/utils/Sorting.kt +++ /dev/null @@ -1,5 +0,0 @@ -package fr.uca.iut.urbandictionarylight.utils - -public enum class Sorting { - ASC, DESC -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..a07919d --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000..f9213d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_done.xml b/app/src/main/res/drawable/ic_done.xml new file mode 100644 index 0000000..99caef9 --- /dev/null +++ b/app/src/main/res/drawable/ic_done.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout-land/toolbar_md_activity_content.xml b/app/src/main/res/layout-land/toolbar_md_activity_content.xml new file mode 100644 index 0000000..db8cb86 --- /dev/null +++ b/app/src/main/res/layout-land/toolbar_md_activity_content.xml @@ -0,0 +1,22 @@ + + + + + + + + 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 05d671d..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_pager.xml b/app/src/main/res/layout/activity_pager.xml new file mode 100644 index 0000000..4e2eb16 --- /dev/null +++ b/app/src/main/res/layout/activity_pager.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_entry.xml b/app/src/main/res/layout/fragment_entry.xml new file mode 100644 index 0000000..9fe8d21 --- /dev/null +++ b/app/src/main/res/layout/fragment_entry.xml @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_list_entry.xml b/app/src/main/res/layout/fragment_list_entry.xml new file mode 100644 index 0000000..56b6ee1 --- /dev/null +++ b/app/src/main/res/layout/fragment_list_entry.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_list_entry.xml b/app/src/main/res/layout/item_list_entry.xml new file mode 100644 index 0000000..8c86529 --- /dev/null +++ b/app/src/main/res/layout/item_list_entry.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/toolbar_activity.xml b/app/src/main/res/layout/toolbar_activity.xml new file mode 100644 index 0000000..cd00391 --- /dev/null +++ b/app/src/main/res/layout/toolbar_activity.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/app/src/main/res/layout/toolbar_md_activity.xml b/app/src/main/res/layout/toolbar_md_activity.xml new file mode 100644 index 0000000..fa9e6ce --- /dev/null +++ b/app/src/main/res/layout/toolbar_md_activity.xml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/app/src/main/res/layout/toolbar_md_activity_content.xml b/app/src/main/res/layout/toolbar_md_activity_content.xml new file mode 100644 index 0000000..59d3295 --- /dev/null +++ b/app/src/main/res/layout/toolbar_md_activity_content.xml @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_entry.xml b/app/src/main/res/menu/fragment_entry.xml new file mode 100644 index 0000000..12879b2 --- /dev/null +++ b/app/src/main/res/menu/fragment_entry.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_list_entry.xml b/app/src/main/res/menu/fragment_list_entry.xml new file mode 100644 index 0000000..95aa0db --- /dev/null +++ b/app/src/main/res/menu/fragment_list_entry.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..fd83c8d --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,10 @@ + + + Urban Dictionary Light + Nouvelle définition + Cliquez pour ajouter une nouvelle définition + Nouveau terme + Cliquez pour ajouter un nouveau terme + Enregistrer + Supprimer + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index beea633..d0008f8 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -2,12 +2,12 @@ + + + + + + + + + + + + + + + + \ No newline at end of file