Absorb and refactor example project

coming up: adapting the layouts and VM, plugging in the API, maybe set up notifications
main
Alexis Drai 1 year ago
parent e064615a9d
commit b4f6e61b77

@ -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"
}

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".UrbanDictionaryLight"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -11,20 +12,22 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.UrbanDictionaryLight"
tools:targetApi="31">
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:exported="true">
android:name=".ui.activity.EntryListActivity"
android:exported="true"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
<activity
android:name=".ui.activity.EntryActivity"
android:parentActivityName=".ui.activity.EntryListActivity" />
<activity
android:name=".ui.activity.EntryPagerActivity"
android:parentActivityName=".ui.activity.EntryListActivity" />
</application>
</manifest>

@ -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)
}
}

@ -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)
}
}

@ -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<Definition>
@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)
}

@ -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<EntryWithDefinitions>
@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)
}

@ -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)
)
)
}
}
}
}

@ -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<List<Definition>>
@Query("SELECT * FROM entries WHERE id = :id")
fun query(id: Long): LiveData<Definition>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entry: Definition)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(entry: Definition)
@Delete
suspend fun delete(entry: Definition)
}

@ -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<List<Definition>> {
Log.i(TAG, "GET ALL DEFINITIONS")
return definitionDao.queryAll()
}
fun findById(definitionId: Long): LiveData<Definition> {
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)
}
}

@ -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<List<EntryWithDefinitions>>
@Query("SELECT * FROM entries WHERE id = :id")
fun query(id: Long): LiveData<EntryWithDefinitions>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(entry: EntryWithDefinitions)
@Update
suspend fun update(entry: EntryWithDefinitions)
@Delete
suspend fun delete(entry: EntryWithDefinitions)
}

@ -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<List<EntryWithDefinitions>> {
Log.i(TAG, "GET ALL ENTRIES")
return entryDao.queryAll()
}
fun findById(entryId: Long): LiveData<EntryWithDefinitions> {
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)
}
}

@ -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<Entry> = 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 ?
}*/

@ -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
)

@ -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<Definition>
) val definitions: List<Definition> = listOf()
)

@ -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()
}

@ -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<FrameLayout>(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()
}
}
}

@ -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<EntryPagerViewModel>()
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<LinearLayout>(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()
}

@ -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
}

@ -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
}
}

@ -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<EntryListViewModel>()
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
}
}

@ -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<EntryWithDefinitions>()
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<EntryWithDefinitions>) {
this.entryList = entryList
notifyDataSetChanged()
}
}

@ -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<EntryWithDefinitions, EntryRecyclerViewAdapter.EntryViewHolder>(
DiffUtilEntryCallback
) {
private object DiffUtilEntryCallback : DiffUtil.ItemCallback<EntryWithDefinitions>() {
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)
}
}

@ -0,0 +1,10 @@
package fr.uca.iut.urbandictionarylight.ui.utils
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@Suppress("UNCHECKED_CAST")
inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T = f() as T
}

@ -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) }
}

@ -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
}

@ -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) }
}
}

@ -1,5 +0,0 @@
package fr.uca.iut.urbandictionarylight.utils
public enum class Sorting {
ASC, DESC
}

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#727272"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
</vector>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal"
android:showDividers="middle"
android:divider="?android:attr/dividerVertical">
<FrameLayout
android:id="@+id/container_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="3" />
<FrameLayout
android:id="@+id/container_fragment_detail"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="7" />
</LinearLayout>

@ -1,82 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SearchView
android:id="@+id/search_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:background="#EEE"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:clipToPadding="false"
android:padding="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_view" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_definition_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:clickable="true"
android:contentDescription="@string/add_def_btn"
android:focusable="true"
app:backgroundTint="#55752F"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_view"
app:srcCompat="@android:drawable/ic_input_add" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_term_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:clickable="true"
android:contentDescription="@string/add_term_btn"
android:focusable="true"
app:backgroundTint="#3E4E83"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_view"
app:srcCompat="@android:drawable/ic_input_add" />
<TextView
android:id="@+id/add_definition_btn_lbl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/add_def_lbl"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/add_definition_btn" />
<TextView
android:id="@+id/add_term_btn_lbl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/add_term_lbl"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/add_term_btn" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/pager_layout">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_activity"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:elevation="@dimen/toolbar_elevation"
app:titleMarginStart="@dimen/icon_title_space" />
</LinearLayout>

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="entryVM"
type="fr.uca.iut.urbandictionarylight.ui.viewmodel.EntryViewModel" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/activity_margin"
tools:context=".ui.activity.EntryActivity">
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="right"
app:constraint_referenced_ids="text_overview,text_gender,text_measurement,text_aggressiveness,text_misc" />
<TextView
android:id="@+id/text_overview"
style="@style/CategoryStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/category_overview"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/edit_dog_name"
style="@style/EditorFieldStyle"
android:layout_width="0dp"
android:hint="@string/hint_dog_name"
android:importantForAutofill="no"
android:inputType="textCapWords"
android:text="@={entryVM.dog.name}"
app:layout_constraintBaseline_toBaselineOf="@+id/text_overview"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier" />
<EditText
android:id="@+id/edit_dog_breed"
style="@style/EditorFieldStyle"
android:layout_width="0dp"
android:hint="@string/hint_dog_breed"
android:importantForAutofill="no"
android:inputType="textCapWords"
android:text="@={entryVM.dog.breed}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/barrier"
app:layout_constraintTop_toBottomOf="@+id/edit_dog_name" />
<TextView
android:id="@+id/text_gender"
style="@style/CategoryStyle"
android:layout_width="wrap_content"
android:text="@string/category_gender"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_dog_breed" />
<Spinner
android:id="@+id/spinner_gender"
android:layout_width="wrap_content"
android:layout_height="@dimen/large_space"
android:entries="@array/array_gender_options"
android:selectedItemPosition="@={Converters.genderToInt(dogVM.dog.gender)}"
android:spinnerMode="dropdown"
app:layout_constraintStart_toEndOf="@id/barrier"
app:layout_constraintTop_toTopOf="@+id/text_gender" />
<TextView
android:id="@+id/text_measurement"
style="@style/CategoryStyle"
android:layout_width="wrap_content"
android:text="@string/category_measurement"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/spinner_gender" />
<EditText
android:id="@+id/edit_dog_weight"
style="@style/EditorFieldStyle"
android:layout_width="0dp"
android:hint="@string/hint_dog_weight"
android:importantForAutofill="no"
android:inputType="numberDecimal"
android:text="@={Converters.floatToString(dogVM.dog.weight)}"
app:layout_constraintBaseline_toBaselineOf="@+id/text_measurement"
app:layout_constraintEnd_toStartOf="@id/label_weight_unit"
app:layout_constraintStart_toEndOf="@id/barrier" />
<TextView
android:id="@+id/label_weight_unit"
style="@style/EditorUnitsStyle"
android:layout_width="wrap_content"
android:text="@string/unit_dog_weight"
app:layout_constraintBaseline_toBaselineOf="@+id/text_measurement"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/edit_dog_weight" />
<TextView
android:id="@+id/text_aggressiveness"
style="@style/CategoryStyle"
android:layout_width="wrap_content"
android:text="@string/category_aggressiveness"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/edit_dog_weight" />
<RatingBar
android:id="@+id/ratingbar_aggressiveness"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:numStars="3"
android:rating="@={(float) dogVM.dog.aggressiveness}"
android:stepSize="1"
app:layout_constraintStart_toEndOf="@id/barrier"
app:layout_constraintTop_toTopOf="@+id/text_aggressiveness" />
<TextView
android:id="@+id/text_misc"
style="@style/CategoryStyle"
android:layout_width="wrap_content"
android:layout_marginTop="@dimen/large_space"
android:text="@string/category_misc"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ratingbar_aggressiveness" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</layout>

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<variable
name="entryListVM"
type="fr.uca.iut.urbandictionarylight.ui.viewmodel.EntryListViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.activity.EntryListActivity">
<SearchView
android:id="@+id/search_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:background="#EEE"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
app:spanCount="2"
tools:listitem="@layout/item_list_entry" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_entry_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:clickable="true"
android:contentDescription="@string/add_entry_btn"
android:focusable="true"
app:backgroundTint="#3E4E83"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/search_view"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="entryWithDefinitions"
type="fr.uca.iut.urbandictionarylight.model.EntryWithDefinitions" />
</data>
<androidx.cardview.widget.CardView
android:id="@+id/cardview_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="2dp"
card_view:cardUseCompatPadding="true">
<LinearLayout
android:id="@+id/layout_cardview_entry"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_margin">
<TextView
android:id="@+id/view_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:text="@{entryWithDefinitions.entry.phrase}"
android:textAppearance="?android:textAppearanceMedium"
android:textColor="@color/black"
tools:text="Entry phrase" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.activity.EntryActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_activity"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/Theme.UrbanDictionaryLight.AppBarOverlay"
app:elevation="@dimen/toolbar_elevation"
app:popupTheme="@style/Theme.UrbanDictionaryLight.PopupOverlay"
app:titleMarginStart="@dimen/icon_title_space" />
<FrameLayout
android:id="@+id/container_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".ui.activity.EntryListActivity">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_activity"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:elevation="@dimen/toolbar_elevation"
android:theme="@style/Theme.UrbanDictionaryLight.AppBarOverlay"
app:popupTheme="@style/Theme.UrbanDictionaryLight.PopupOverlay"
app:titleMarginStart="@dimen/icon_title_space" />
<include layout="@layout/toolbar_md_activity_content" />
</LinearLayout>

@ -0,0 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_done"
android:title="@string/save_lbl"
app:showAsAction="always"/>
<item
android:id="@+id/action_delete"
android:icon="@drawable/ic_delete"
android:title="@string/delete_lbl"
app:showAsAction="ifRoom"/>
</menu>

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_item_new_entry"
android:icon="@drawable/ic_add"
android:title="@string/add_entry_lbl"
app:showAsAction="never"/>
</menu>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Urban Dictionary Light</string>
<string name="add_def_lbl">Nouvelle définition</string>
<string name="add_def_btn">Cliquez pour ajouter une nouvelle définition</string>
<string name="add_entry_lbl">Nouveau terme</string>
<string name="add_entry_btn">Cliquez pour ajouter un nouveau terme</string>
<string name="save_lbl">Enregistrer</string>
<string name="delete_lbl">Supprimer</string>
</resources>

@ -2,12 +2,12 @@
<!-- Base application theme. -->
<style name="Theme.UrbanDictionaryLight" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">@color/teal_200</item>
<item name="colorPrimaryVariant">@color/teal_900</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorSecondary">@color/blue_200</item>
<item name="colorSecondaryVariant">@color/blue_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>

@ -1,10 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="teal_700">#0097A7</color>
<color name="teal_900">#006064</color>
<color name="blue_200">#62A3FF</color>
<color name="blue_700">#1675D1</color>
<color name="blue_900">#004A9F</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Common margin value used throughout the app -->
<dimen name="activity_margin">16dp</dimen>
<dimen name="text_margin">16dp</dimen>
<dimen name="toolbar_elevation">4dp</dimen>
<!-- Common spaces used throughout the app -->
<dimen name="large_space">32dp</dimen>
<dimen name="medium_space">16dp</dimen>
<dimen name="small_space">8dp</dimen>
<dimen name="icon_title_space">24dp</dimen>
</resources>

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="view_pager" type="id" />
</resources>

@ -1,7 +1,12 @@
<resources>
<string name="app_name">Urban Dictionary Light</string>
<string name="add_def_lbl">New definition</string>
<string name="add_term_lbl">New term</string>
<string name="add_term_btn">Click to add a new term</string>
<string name="add_def_btn">Click to add a new definition</string>
<string name="add_entry_lbl">New term</string>
<string name="add_entry_btn">Click to add a new term</string>
<string name="save_lbl">Save</string>
<string name="delete_lbl">Delete</string>
<string name="create_entry_error_dialog_title" translatable="false">That\'s a woopsie</string>
<string name="create_entry_error_message" translatable="false">sorry bud, no can do</string>
</resources>

@ -2,15 +2,63 @@
<!-- Base application theme. -->
<style name="Theme.UrbanDictionaryLight" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorPrimary">@color/teal_700</item>
<item name="colorPrimaryVariant">@color/teal_900</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorSecondary">@color/blue_200</item>
<item name="colorSecondaryVariant">@color/blue_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
<style name="Theme.UrbanDictionaryLight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="Theme.UrbanDictionaryLight.AppBarOverlay" parent="ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<style name="Theme.UrbanDictionaryLight.PopupOverlay" parent="ThemeOverlay.MaterialComponents.Light" />
<!-- A narrower dialog (wrap its content) than the default MaterialComponents theme
Useful to correctly display the DatePicker Dialog without extra blank space -->
<style name="NarrowDialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert">
<item name="android:windowMinWidthMajor">0dp</item>
<item name="android:windowMinWidthMinor">0dp</item>
</style>
<!-- Style for a category in the editor -->
<style name="CategoryStyle">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">0dp</item>
<item name="android:layout_weight">1</item>
<item name="android:paddingTop">@dimen/medium_space</item>
<item name="android:paddingRight">@dimen/medium_space</item>
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textAppearance">?android:textAppearanceSmall</item>
</style>
<!-- Style for an EditText field in the editor -->
<style name="EditorFieldStyle">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">match_parent</item>
<item name="android:textAppearance">?android:textAppearanceMedium</item>
</style>
<!-- Style for a TextView in the editor -->
<style name="EditorTextStyle">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:textAppearance">?android:textAppearanceMedium</item>
</style>
<!-- Style for the measurement units for an EditText field in the editor -->
<style name="EditorUnitsStyle">
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:textAppearance">?android:textAppearanceSmall</item>
</style>
</resources>
Loading…
Cancel
Save