diff --git a/.gitignore b/.gitignore index bd39932..badd818 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ captures/ .idea/libraries .idea/caches .idea/sonarlint +.idea/markdown-navigator.xml +.idea/markdown-navigator # Keystore files # Uncomment the following line if you do not want to check your keystore files in. diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 30aa626..4edc5b9 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,29 +1,232 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..15a15b2 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 109feca..4c2eb3a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,9 @@ android { applicationId "app.marcdev.earworm" minSdkVersion 23 targetSdkVersion 28 - versionCode 12 - versionName "1.0.0" + versionCode 15 + versionName "1.1.0" + setProperty("archivesBaseName", "Earworm-v$versionName") testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -30,14 +31,23 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.0.2' + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1' + + // AndroidX + implementation 'androidx.appcompat:appcompat:1.1.0-alpha02' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha5' implementation 'androidx.preference:preference:1.0.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2' implementation 'com.google.android.material:material:1.0.0' + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha01" + + // Testing testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' // Timber for logging implementation('com.jakewharton.timber:timber:4.7.0') @@ -47,14 +57,14 @@ dependencies { kapt 'androidx.room:room-compiler:2.0.0' androidTestImplementation 'androidx.room:room-testing:2.0.0' - // Kotlin co-routines for asynchronous code - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0' - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.0' + // Kodein + implementation 'org.kodein.di:kodein-di-generic-jvm:6.0.1' + implementation 'org.kodein.di:kodein-di-framework-android-x:6.0.1' // Glide for image loading and caching - implementation 'com.github.bumptech.glide:glide:4.8.0' + implementation 'com.github.bumptech.glide:glide:4.9.0' kapt 'com.github.bumptech.glide:compiler:4.8.0' // Android File Picker for choosing an image - implementation 'com.droidninja:filepicker:2.2.0' + implementation 'com.droidninja:filepicker:2.2.1' } diff --git a/app/src/androidTest/java/app/marcdev/earworm/database/DAOTest.kt b/app/src/androidTest/java/app/marcdev/earworm/data/database/DAOTest.kt similarity index 88% rename from app/src/androidTest/java/app/marcdev/earworm/database/DAOTest.kt rename to app/src/androidTest/java/app/marcdev/earworm/data/database/DAOTest.kt index f7311c2..1c09362 100644 --- a/app/src/androidTest/java/app/marcdev/earworm/database/DAOTest.kt +++ b/app/src/androidTest/java/app/marcdev/earworm/data/database/DAOTest.kt @@ -1,4 +1,4 @@ -package app.marcdev.earworm.database +package app.marcdev.earworm.data.database import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry @@ -10,7 +10,7 @@ import org.junit.Test class DAOTest { - private var database: AppDatabase? = null + private var database: ProductionAppDatabase? = null private var dao: DAO? = null // Default values @@ -27,7 +27,7 @@ class DAOTest { @Before fun setUp() { database = - Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().context, AppDatabase::class.java) + Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().context, ProductionAppDatabase::class.java) .allowMainThreadQueries().build() dao = database!!.dao() } @@ -48,7 +48,7 @@ class DAOTest { val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - database?.dao()!!.insertOrUpdateItem(testItem) + database?.dao()!!.insertItem(testItem) val returnedItemsWhenOneInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(1, returnedItemsWhenOneInserted.size) @@ -62,8 +62,8 @@ class DAOTest { val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - database?.dao()!!.insertOrUpdateItem(testItem1) - database?.dao()!!.insertOrUpdateItem(testItem2) + database?.dao()!!.insertItem(testItem1) + database?.dao()!!.insertItem(testItem2) val returnedItemsWhenOneInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(2, returnedItemsWhenOneInserted.size) @@ -79,7 +79,7 @@ class DAOTest { val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getItemById(testId) Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - database?.dao()!!.insertOrUpdateItem(testItem) + database?.dao()!!.insertItem(testItem) val returnedItemsWhenOneInserted: MutableList = database?.dao()!!.getItemById(testId) Assert.assertEquals(1, returnedItemsWhenOneInserted.size) @@ -103,8 +103,8 @@ class DAOTest { val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - database?.dao()!!.insertOrUpdateItem(testItem1) - database?.dao()!!.insertOrUpdateItem(testItem2) + database?.dao()!!.insertItem(testItem1) + database?.dao()!!.insertItem(testItem2) val returnedItemsWhenTwoInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(2, returnedItemsWhenTwoInserted.size) @@ -131,8 +131,8 @@ class DAOTest { val returnedItemsWhenNothingInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - database?.dao()!!.insertOrUpdateItem(testItem1) - database?.dao()!!.insertOrUpdateItem(testItem2) + database?.dao()!!.insertItem(testItem1) + database?.dao()!!.insertItem(testItem2) val returnedAllItemsWhenTwoInserted: MutableList = database?.dao()!!.getAllItems() Assert.assertEquals(2, returnedAllItemsWhenTwoInserted.size) @@ -155,9 +155,9 @@ class DAOTest { val testItem3 = createTestItem() testItem3.imageName = testImage2 - database?.dao()!!.insertOrUpdateItem(testItem1) - database?.dao()!!.insertOrUpdateItem(testItem2) - database?.dao()!!.insertOrUpdateItem(testItem3) + database?.dao()!!.insertItem(testItem1) + database?.dao()!!.insertItem(testItem2) + database?.dao()!!.insertItem(testItem3) val returnedValueWhenSearchedForTestImage1: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1) Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1) @@ -186,9 +186,9 @@ class DAOTest { testItem3.imageName = testImage2 testItem3.id = testId3 - database?.dao()!!.insertOrUpdateItem(testItem1) - database?.dao()!!.insertOrUpdateItem(testItem2) - database?.dao()!!.insertOrUpdateItem(testItem3) + database?.dao()!!.insertItem(testItem1) + database?.dao()!!.insertItem(testItem2) + database?.dao()!!.insertItem(testItem3) val returnedValueWhenSearchedForTestImage1: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1) Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1) @@ -225,9 +225,9 @@ class DAOTest { testItem3.imageName = testImage2 testItem3.id = testId3 - database?.dao()!!.insertOrUpdateItem(testItem1) - database?.dao()!!.insertOrUpdateItem(testItem2) - database?.dao()!!.insertOrUpdateItem(testItem3) + database?.dao()!!.insertItem(testItem1) + database?.dao()!!.insertItem(testItem2) + database?.dao()!!.insertItem(testItem3) val returnedValueWhenSearchedForTestImage1: Int = database?.dao()!!.getNumberOfEntriesUsingImage(testImage1) Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1) diff --git a/app/src/androidTest/java/app/marcdev/earworm/repository/FavouriteItemRepositoryTest.kt b/app/src/androidTest/java/app/marcdev/earworm/data/repository/FavouriteItemRepositoryTest.kt similarity index 87% rename from app/src/androidTest/java/app/marcdev/earworm/repository/FavouriteItemRepositoryTest.kt rename to app/src/androidTest/java/app/marcdev/earworm/data/repository/FavouriteItemRepositoryTest.kt index d56cfce..beabca3 100644 --- a/app/src/androidTest/java/app/marcdev/earworm/repository/FavouriteItemRepositoryTest.kt +++ b/app/src/androidTest/java/app/marcdev/earworm/data/repository/FavouriteItemRepositoryTest.kt @@ -1,10 +1,10 @@ -package app.marcdev.earworm.repository +package app.marcdev.earworm.data.repository import androidx.room.Room import androidx.test.platform.app.InstrumentationRegistry -import app.marcdev.earworm.database.AppDatabase -import app.marcdev.earworm.database.DAO -import app.marcdev.earworm.database.FavouriteItem +import app.marcdev.earworm.data.database.ProductionAppDatabase +import app.marcdev.earworm.data.database.DAO +import app.marcdev.earworm.data.database.FavouriteItem import app.marcdev.earworm.utils.SONG import kotlinx.coroutines.runBlocking import org.junit.After @@ -14,7 +14,7 @@ import org.junit.Test class FavouriteItemRepositoryTest { - private var database: AppDatabase? = null + private var database: ProductionAppDatabase? = null private var repository: FavouriteItemRepository? = null private var dao: DAO? = null @@ -32,7 +32,7 @@ class FavouriteItemRepositoryTest { @Before fun setUp() { database = - Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().context, AppDatabase::class.java) + Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getInstrumentation().context, ProductionAppDatabase::class.java) .allowMainThreadQueries().build() dao = database!!.dao() @@ -56,8 +56,8 @@ class FavouriteItemRepositoryTest { val returnedItemsWhenNothingInserted: MutableList = repository!!.getAllItems() Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - repository!!.insertOrUpdateItem(testItem1) - repository!!.insertOrUpdateItem(testItem2) + repository!!.addItem(testItem1) + repository!!.addItem(testItem2) val returnedItemsWhenOneInserted: MutableList = repository!!.getAllItems() Assert.assertEquals(2, returnedItemsWhenOneInserted.size) @@ -73,7 +73,7 @@ class FavouriteItemRepositoryTest { val returnedItemsWhenNothingInserted: MutableList = repository!!.getItem(testId) Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - repository!!.insertOrUpdateItem(testItem) + repository!!.addItem(testItem) val returnedItemsWhenOneInserted: MutableList = repository!!.getItem(testId) Assert.assertEquals(1, returnedItemsWhenOneInserted.size) @@ -97,8 +97,8 @@ class FavouriteItemRepositoryTest { val returnedItemsWhenNothingInserted: MutableList = repository!!.getAllItems() Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - repository!!.insertOrUpdateItem(testItem1) - repository!!.insertOrUpdateItem(testItem2) + repository!!.addItem(testItem1) + repository!!.addItem(testItem2) val returnedItemsWhenTwoInserted: MutableList = repository!!.getAllItems() Assert.assertEquals(2, returnedItemsWhenTwoInserted.size) @@ -125,8 +125,8 @@ class FavouriteItemRepositoryTest { val returnedItemsWhenNothingInserted: MutableList = repository!!.getAllItems() Assert.assertEquals(0, returnedItemsWhenNothingInserted.size) - repository!!.insertOrUpdateItem(testItem1) - repository!!.insertOrUpdateItem(testItem2) + repository!!.addItem(testItem1) + repository!!.addItem(testItem2) val returnedAllItemsWhenTwoInserted: MutableList = repository!!.getAllItems() Assert.assertEquals(2, returnedAllItemsWhenTwoInserted.size) @@ -149,9 +149,9 @@ class FavouriteItemRepositoryTest { val testItem3 = createTestItem() testItem3.imageName = testImage2 - repository!!.insertOrUpdateItem(testItem1) - repository!!.insertOrUpdateItem(testItem2) - repository!!.insertOrUpdateItem(testItem3) + repository!!.addItem(testItem1) + repository!!.addItem(testItem2) + repository!!.addItem(testItem3) val returnedValueWhenSearchedForTestImage1: Int = repository!!.countUsesOfImage(testImage1) Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1) @@ -180,9 +180,9 @@ class FavouriteItemRepositoryTest { testItem3.imageName = testImage2 testItem3.id = testId3 - repository!!.insertOrUpdateItem(testItem1) - repository!!.insertOrUpdateItem(testItem2) - repository!!.insertOrUpdateItem(testItem3) + repository!!.addItem(testItem1) + repository!!.addItem(testItem2) + repository!!.addItem(testItem3) val returnedValueWhenSearchedForTestImage1: Int = repository!!.countUsesOfImage(testImage1) Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1) @@ -219,9 +219,9 @@ class FavouriteItemRepositoryTest { testItem3.imageName = testImage2 testItem3.id = testId3 - repository!!.insertOrUpdateItem(testItem1) - repository!!.insertOrUpdateItem(testItem2) - repository!!.insertOrUpdateItem(testItem3) + repository!!.addItem(testItem1) + repository!!.addItem(testItem2) + repository!!.addItem(testItem3) val returnedValueWhenSearchedForTestImage1: Int = repository!!.countUsesOfImage(testImage1) Assert.assertEquals(1, returnedValueWhenSearchedForTestImage1) diff --git a/app/src/main/java/app/marcdev/earworm/Earworm.kt b/app/src/main/java/app/marcdev/earworm/Earworm.kt index ccbdf72..2678b54 100644 --- a/app/src/main/java/app/marcdev/earworm/Earworm.kt +++ b/app/src/main/java/app/marcdev/earworm/Earworm.kt @@ -1,13 +1,47 @@ package app.marcdev.earworm import android.app.Application +import app.marcdev.earworm.additem.AddItemViewModelFactory +import app.marcdev.earworm.data.database.AppDatabase +import app.marcdev.earworm.data.database.DAO +import app.marcdev.earworm.data.database.ProductionAppDatabase +import app.marcdev.earworm.data.repository.FavouriteItemRepository +import app.marcdev.earworm.data.repository.FavouriteItemRepositoryImpl +import app.marcdev.earworm.mainscreen.MainFragmentViewModelFactory +import app.marcdev.earworm.utils.FileUtils +import app.marcdev.earworm.utils.FileUtilsImpl +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.androidXModule +import org.kodein.di.generic.bind +import org.kodein.di.generic.instance +import org.kodein.di.generic.provider +import org.kodein.di.generic.singleton import timber.log.Timber -class Earworm : Application() { +class Earworm : Application(), KodeinAware { + override val kodein = Kodein.lazy { + import(androidXModule(this@Earworm)) + + // + bind() with singleton { ProductionAppDatabase.invoke(applicationContext) } + bind() with singleton { instance().dao() } + bind() with singleton { FavouriteItemRepositoryImpl.getInstance(instance()) } + // + + // + bind() with provider { FileUtilsImpl(instance()) } + // + + // + bind() from provider { MainFragmentViewModelFactory(instance(), instance()) } + bind() from provider { AddItemViewModelFactory(instance(), instance()) } + // + } override fun onCreate() { super.onCreate() - if (BuildConfig.DEBUG) { + if(BuildConfig.DEBUG) { Timber.plant(Timber.DebugTree()) Timber.i("Log: Timber Debug Tree planted") } diff --git a/app/src/main/java/app/marcdev/earworm/MainActivity.kt b/app/src/main/java/app/marcdev/earworm/MainActivity.kt index 4a56954..cfff7ab 100644 --- a/app/src/main/java/app/marcdev/earworm/MainActivity.kt +++ b/app/src/main/java/app/marcdev/earworm/MainActivity.kt @@ -1,33 +1,28 @@ package app.marcdev.earworm import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity import androidx.coordinatorlayout.widget.CoordinatorLayout -import app.marcdev.earworm.mainscreen.MainFragmentViewImpl -import app.marcdev.earworm.utils.DARK_THEME -import app.marcdev.earworm.utils.LIGHT_THEME +import app.marcdev.earworm.internal.DARK_THEME +import app.marcdev.earworm.internal.LIGHT_THEME +import app.marcdev.earworm.internal.base.EarwormActivity +import app.marcdev.earworm.mainscreen.MainFragment import app.marcdev.earworm.utils.getTheme -import app.marcdev.earworm.utils.setFragment import timber.log.Timber -class MainActivity : AppCompatActivity() { +class MainActivity : EarwormActivity() { private lateinit var mainFrame: CoordinatorLayout private var activityTheme: Int = -1 override fun onCreate(savedInstanceState: Bundle?) { - Timber.d("Log: onCreate: Started") - /* Theme changes must be done before super.onCreate otherwise it will be overridden with the value in the manifest */ - if(getTheme(applicationContext) == DARK_THEME) { - Timber.v("Log: onCreate: Is dark mode") + activityTheme = if(getTheme(applicationContext) == DARK_THEME) { setTheme(R.style.Earworm_DarkTheme) - activityTheme = DARK_THEME + DARK_THEME } else { - Timber.v("Log: onCreate: Is not dark mode") setTheme(R.style.Earworm_LightTheme) - activityTheme = LIGHT_THEME + LIGHT_THEME } super.onCreate(savedInstanceState) @@ -39,13 +34,11 @@ class MainActivity : AppCompatActivity() { } private fun bindViews() { - Timber.v("Log: bindViews: Started") this.mainFrame = findViewById(R.id.frame_main) } private fun setDefaultFragment() { - Timber.v("Log: setDefaultFragment: Started") - val fragment = MainFragmentViewImpl() + val fragment = MainFragment() if(intent.action == "app.marcdev.earworm.intent.ADD_ITEM") { Timber.d("Log: onCreate: Started from Add Item app shortcut") @@ -60,7 +53,6 @@ class MainActivity : AppCompatActivity() { override fun onResume() { super.onResume() if(getTheme(applicationContext) != activityTheme) { - Timber.d("Log: onResume: Theme was changed, recreating activity") recreate() } } diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemBottomSheet.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemBottomSheet.kt index 27b8660..1dfe1b2 100644 --- a/app/src/main/java/app/marcdev/earworm/additem/AddItemBottomSheet.kt +++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemBottomSheet.kt @@ -2,198 +2,119 @@ package app.marcdev.earworm.additem import android.Manifest import android.app.Activity -import android.app.Dialog import android.content.Intent import android.content.pm.PackageManager -import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.preference.PreferenceManager import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.EditText +import android.widget.ImageView +import android.widget.Toast import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.uicomponents.RoundedBottomDialogFragment -import app.marcdev.earworm.utils.* +import app.marcdev.earworm.internal.ALBUM +import app.marcdev.earworm.internal.ARTIST +import app.marcdev.earworm.internal.PREF_CLEAR_INPUTS +import app.marcdev.earworm.internal.SONG +import app.marcdev.earworm.internal.base.EarwormBottomSheetDialogFragment +import app.marcdev.earworm.uicomponents.AddItemDatePickerDialog +import app.marcdev.earworm.uicomponents.BinaryOptionDialog +import app.marcdev.earworm.utils.changeColorOfDrawable +import app.marcdev.earworm.utils.changeColorOfImageViewDrawable import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip import droidninja.filepicker.FilePickerBuilder import droidninja.filepicker.FilePickerConst -import timber.log.Timber -import java.util.* +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.closestKodein +import org.kodein.di.generic.instance -class AddItemBottomSheet : RoundedBottomDialogFragment(), AddItemView { +class AddItemBottomSheet : EarwormBottomSheetDialogFragment(), KodeinAware { + override val kodein: Kodein by closestKodein() + // + private val viewModelFactory: AddItemViewModelFactory by instance() + private lateinit var viewModel: AddItemViewModel + // + + // private lateinit var saveButton: MaterialButton private lateinit var primaryInput: EditText private lateinit var secondaryInput: EditText - private lateinit var songButton: ImageButton - private lateinit var albumButton: ImageButton - private lateinit var artistButton: ImageButton - private lateinit var presenter: AddItemPresenter - private lateinit var datePickerDialog: Dialog - private lateinit var datePicker: DatePicker - private lateinit var datePickerOk: MaterialButton - private lateinit var datePickerCancel: MaterialButton + private lateinit var songButton: ImageView + private lateinit var albumButton: ImageView + private lateinit var artistButton: ImageView + private lateinit var datePickerDialog: AddItemDatePickerDialog private lateinit var iconImageView: ImageView private lateinit var dateChip: Chip - private lateinit var confirmDeleteDialog: Dialog - private val dateChosen = Calendar.getInstance() - // If the itemID is anything other than -1 then it is in edit mode - private var itemId: Int = -1 + private lateinit var confirmImageDeleteDialog: BinaryOptionDialog + // - private var type: Int = 0 - private var recyclerUpdateView: RecyclerUpdateView? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel = ViewModelProviders.of(this, viewModelFactory).get(AddItemViewModel::class.java) + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(R.layout.dialog_add_item, container, false) - presenter = AddItemPresenterImpl(this, activity!!.applicationContext) bindViews(view) + setupObservers() - arguments?.let { - Timber.d("Log: onCreateView: Arguments not null") - this.itemId = arguments!!.getInt("item_id") - presenter.getItem(itemId) - } ?: run { - Timber.d("Log: onCreateView: Arguments null") - setupDefaults() + var itemId = -1 + if(arguments != null) { + itemId = requireArguments().getInt("item_id", -1) } - return view - } + viewModel.passArguments(itemId) - override fun convertToEditMode(item: FavouriteItem) { - Timber.d("Log: convertToEditMode: Started") - - when(item.type) { - SONG -> { - activateButton(songButton) - primaryInput.setText(item.songName) - secondaryInput.setText(item.artistName) - } - - ALBUM -> { - activateButton(albumButton) - primaryInput.setText(item.albumName) - secondaryInput.setText(item.artistName) - } - - ARTIST -> { - activateButton(artistButton) - primaryInput.setText(item.artistName) - secondaryInput.setText(item.genre) - } - } - - if(item.imageName.isNotBlank()) { - presenter.updateFilePath(getArtworkDirectory(requireContext()) + item.imageName) - } - updateDateAndDisplay(item.day, item.month, item.year) - } - - fun bindRecyclerUpdateView(view: RecyclerUpdateView) { - Timber.d("Log: bindRecyclerUpdateView: Started") - this.recyclerUpdateView = view + return view } private fun bindViews(view: View) { - Timber.v("Log: bindViews: Started") - this.saveButton = view.findViewById(R.id.btn_add_item_save) - saveButton.setOnClickListener(saveOnClickListener) - - this.primaryInput = view.findViewById(R.id.edt_item_primary_input) - this.secondaryInput = view.findViewById(R.id.edt_item_secondary_input) - - this.songButton = view.findViewById(R.id.btn_add_item_song_choice) - songButton.setOnClickListener(songOnClickListener) - - this.albumButton = view.findViewById(R.id.btn_add_item_album_choice) - albumButton.setOnClickListener(albumOnClickListener) + saveButton = view.findViewById(R.id.btn_add_item_save) + saveButton.setOnClickListener { + viewModel.save(primaryInput.text.toString(), secondaryInput.text.toString()) + } - this.artistButton = view.findViewById(R.id.btn_add_item_artist_choice) - artistButton.setOnClickListener(artistOnClickListener) + primaryInput = view.findViewById(R.id.edt_item_primary_input) + secondaryInput = view.findViewById(R.id.edt_item_secondary_input) - this.datePickerDialog = Dialog(this.requireActivity()) - datePickerDialog.setContentView(R.layout.dialog_datepicker) - datePickerDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + songButton = view.findViewById(R.id.btn_add_item_song_choice) + songButton.setOnClickListener { viewModel.setType(SONG) } - this.datePicker = datePickerDialog.findViewById(R.id.datepicker) + albumButton = view.findViewById(R.id.btn_add_item_album_choice) + albumButton.setOnClickListener { viewModel.setType(ALBUM) } - this.datePickerOk = datePickerDialog.findViewById(R.id.btn_datepicker_ok) - datePickerOk.setOnClickListener(dateOnOkClickListener) + artistButton = view.findViewById(R.id.btn_add_item_artist_choice) + artistButton.setOnClickListener { viewModel.setType(ARTIST) } - this.datePickerCancel = datePickerDialog.findViewById(R.id.btn_datepicker_cancel) - datePickerCancel.setOnClickListener(dateOnCancelClickListener) + datePickerDialog = AddItemDatePickerDialog(viewModel.date) { day, month, year -> + viewModel.setDate(day, month, year) + datePickerDialog.dismiss() + } - this.dateChip = view.findViewById(R.id.chip_add_item_date_display) - dateChip.setOnClickListener(dateOnClickListener) + dateChip = view.findViewById(R.id.chip_add_item_date_display) + dateChip.setOnClickListener { + datePickerDialog.show(requireFragmentManager(), "Add Item Date Picker Dialog") + } - this.iconImageView = view.findViewById(R.id.img_add_icon) + iconImageView = view.findViewById(R.id.img_add_icon) iconImageView.setOnClickListener(iconOnClickListener) - iconImageView.setOnLongClickListener(iconOnLongClickListener) + iconImageView.setOnLongClickListener { confirmImageDeleteDialog.show(requireFragmentManager(), "Confirm Image Delete Dialog"); true } // Convert to dark mode if needed changeColorOfDrawable(requireContext(), iconImageView.drawable, false) - initEditDialog() - } - - private val saveOnClickListener = View.OnClickListener { - Timber.d("Log: SaveClick: Clicked") - if(itemId == -1) { - Timber.d("Log: saveOnClickListener: Adding new item") - presenter.addItem(primaryInput.text.toString(), secondaryInput.text.toString(), type, dateChosen, null) - } else { - Timber.d("Log: saveOnClickListener: Updating item with id = $itemId") - presenter.addItem(primaryInput.text.toString(), secondaryInput.text.toString(), type, dateChosen, itemId) - } - } - - private val dateOnClickListener = View.OnClickListener { - Timber.d("Log: DateClick: Clicked") - datePicker.updateDate(dateChosen.get(Calendar.YEAR), dateChosen.get(Calendar.MONTH), dateChosen.get(Calendar.DAY_OF_MONTH)) - datePickerDialog.show() - } - - private val dateOnOkClickListener = View.OnClickListener { - Timber.d("Log: DateOkClick: Clicked") - - val day = datePicker.dayOfMonth - val monthRaw = datePicker.month - val year = datePicker.year - - updateDateAndDisplay(day, monthRaw, year) - - datePickerDialog.dismiss() - } - - private val dateOnCancelClickListener = View.OnClickListener { - Timber.d("Log: DateCancelClick: Clicked") - datePickerDialog.dismiss() - } - - private val songOnClickListener = View.OnClickListener { - Timber.d("Log: SongClick: Clicked") - activateButtonIfNecessary(songButton) - } - - private val albumOnClickListener = View.OnClickListener { - Timber.d("Log: AlbumClick: Clicked") - activateButtonIfNecessary(albumButton) - } - - private val artistOnClickListener = View.OnClickListener { - Timber.d("Log: ArtistClick: Clicked") - activateButtonIfNecessary(artistButton) + initImageDeleteDialog() } private val iconOnClickListener = View.OnClickListener { - Timber.d("Log: IconClick: Started") - if(ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { askForStoragePermissions() } else { @@ -204,37 +125,19 @@ class AddItemBottomSheet : RoundedBottomDialogFragment(), AddItemView { } } - private val iconOnLongClickListener = View.OnLongClickListener { - Timber.d("Log: IconLongClick: Started") - - confirmDeleteDialog.show() - return@OnLongClickListener true - } - - private fun initEditDialog() { - this.confirmDeleteDialog = Dialog(requireContext()) - confirmDeleteDialog.setContentView(R.layout.dialog_delete_image) - confirmDeleteDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - val confirmDeleteButton = confirmDeleteDialog.findViewById(R.id.btn_delete_image_confirm) - confirmDeleteButton.setOnClickListener(confirmDeleteOnClickListener) - val cancelButton = confirmDeleteDialog.findViewById(R.id.btn_delete_image_cancel) - cancelButton.setOnClickListener(cancelDeleteOnClickListener) - } - - private val confirmDeleteOnClickListener = View.OnClickListener { - Timber.d("Log: ConfirmDelete: Clicked") - presenter.updateFilePath("") - confirmDeleteDialog.dismiss() - } - - private val cancelDeleteOnClickListener = View.OnClickListener { - Timber.d("Log: CancelDelete: Clicked") - confirmDeleteDialog.dismiss() + private fun initImageDeleteDialog() { + val dialogBuilder = BinaryOptionDialog.Builder() + dialogBuilder + .setTitle(resources.getString(R.string.confirm_image_deletion)) + .setMessageVisible(false) + .setPositiveButton(resources.getString(R.string.cancel), {}, true) + .setNegativeButton(resources.getString(R.string.delete), { + viewModel.removeImage() + }, true) + confirmImageDeleteDialog = dialogBuilder.build() } private fun askForStoragePermissions() { - Timber.d("Log: askForStoragePermissions: Started") ActivityCompat.requestPermissions(requireActivity(), arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) } @@ -244,66 +147,100 @@ class AddItemBottomSheet : RoundedBottomDialogFragment(), AddItemView { if(requestCode == FilePickerConst.REQUEST_CODE_PHOTO && resultCode == Activity.RESULT_OK && data != null) { val photoPathArray = data.getStringArrayListExtra(FilePickerConst.KEY_SELECTED_MEDIA) val photoPath = photoPathArray[0] - presenter.updateFilePath(photoPath) - - displayImage(photoPath) + viewModel.setNewImage(photoPath) } } - private fun setupDefaults() { - Timber.d("Log: setupDefaults: Started") - type = SONG - changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, true) - changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, false) - changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, false) - dateChip.text = resources.getString(R.string.today) - } + private fun setupObservers() { + viewModel.displayAdded.observe(this, Observer { value -> + value?.let { show -> + if(show) + Toast.makeText(requireActivity(), resources.getString(R.string.item_added), Toast.LENGTH_SHORT).show() + } + }) - private fun activateButtonIfNecessary(button: ImageButton) { - Timber.d("Log: activateButtonIfNecessary: Started") + viewModel.displayEmpty.observe(this, Observer { value -> + value?.let { show -> + if(show) + Toast.makeText(requireActivity(), resources.getString(R.string.empty), Toast.LENGTH_SHORT).show() + } + }) - if(type == SONG && button == songButton - || type == ALBUM && button == albumButton - || type == ARTIST && button == artistButton - ) { - Timber.d("Log: activateButtonIfNecessary: No need to update") - } else { - activateButton(button) - } - } + viewModel.displayError.observe(this, Observer { value -> + value?.let { show -> + if(show) + Toast.makeText(requireActivity(), resources.getString(R.string.error), Toast.LENGTH_SHORT).show() + } + }) + + viewModel.selectedType.observe(this, Observer { value -> + value?.let { type -> + setTypeSelected(type) + } + }) + + viewModel.dateDisplay.observe(this, Observer { value -> + value?.let { dateToDisplay -> + if(dateToDisplay.isBlank()) + dateChip.text = resources.getString(R.string.today) + else + dateChip.text = dateToDisplay + } + }) + + viewModel.primaryInputDisplay.observe(this, Observer { value -> + value?.let { input -> + primaryInput.setText(input) + primaryInput.setSelection(input.length) + } + }) - private fun activateButton(button: ImageButton) { - Timber.d("Log: activateButtonIfNecessary: Activating button $button") + viewModel.secondaryInputDisplay.observe(this, Observer { value -> + value?.let { input -> + secondaryInput.setText(input) + } + }) + + viewModel.displayImage.observe(this, Observer { value -> + value?.let { filePath -> + displayImage(filePath) + } + }) + + viewModel.dismiss.observe(this, Observer { value -> + value?.let { dismiss -> + if(dismiss) + dismiss() + } + }) + } - when(button) { - songButton -> { - changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, true) - changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, false) - changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, false) - type = SONG + private fun setTypeSelected(type: Int) { + when(type) { + SONG -> { + changeColorOfImageViewDrawable(requireActivity(), songButton, true) + changeColorOfImageViewDrawable(requireActivity(), albumButton, false) + changeColorOfImageViewDrawable(requireActivity(), artistButton, false) primaryInput.hint = resources.getString(R.string.song_name) secondaryInput.hint = resources.getString(R.string.artist) } - - albumButton -> { - changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, false) - changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, true) - changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, false) - type = ALBUM + ALBUM -> { + changeColorOfImageViewDrawable(requireActivity(), songButton, false) + changeColorOfImageViewDrawable(requireActivity(), albumButton, true) + changeColorOfImageViewDrawable(requireActivity(), artistButton, false) primaryInput.hint = resources.getString(R.string.album) secondaryInput.hint = resources.getString(R.string.artist) } - - artistButton -> { - changeColorOfImageButtonDrawable(activity!!.applicationContext, songButton, false) - changeColorOfImageButtonDrawable(activity!!.applicationContext, albumButton, false) - changeColorOfImageButtonDrawable(activity!!.applicationContext, artistButton, true) - type = ARTIST + ARTIST -> { + changeColorOfImageViewDrawable(requireActivity(), songButton, false) + changeColorOfImageViewDrawable(requireActivity(), albumButton, false) + changeColorOfImageViewDrawable(requireActivity(), artistButton, true) primaryInput.hint = resources.getString(R.string.artist) secondaryInput.hint = resources.getString(R.string.genre) } } + // TODO convert to switch preference that uses boolean if(PreferenceManager.getDefaultSharedPreferences(requireContext()).getString(PREF_CLEAR_INPUTS, resources.getString(R.string.yes)) == resources.getString(R.string.yes)) { primaryInput.setText("") secondaryInput.setText("") @@ -311,51 +248,7 @@ class AddItemBottomSheet : RoundedBottomDialogFragment(), AddItemView { } } - private fun updateDateAndDisplay(day: Int, month: Int, year: Int) { - val todayCalendar = Calendar.getInstance() - val todayDay = todayCalendar.get(Calendar.DAY_OF_MONTH) - val todayMonthRaw = todayCalendar.get(Calendar.MONTH) - val todayYear = todayCalendar.get(Calendar.YEAR) - - if(day == todayDay - && month == todayMonthRaw - && year == todayYear - ) { - // Display "Today" on chip - datePickerDialog.dismiss() - dateChip.text = resources.getString(R.string.today) - setDate(todayDay, todayMonthRaw, todayYear) - } else { - - val date = formatDateForDisplay(day, month, year) - dateChip.text = date - - setDate(day, month, year) - datePickerDialog.dismiss() - } - } - - private fun setDate(day: Int, month: Int, year: Int) { - Timber.d("Log: setDate: Started with day = $day, month = $month, year = $year") - dateChosen.set(Calendar.DAY_OF_MONTH, day) - dateChosen.set(Calendar.MONTH, month) - dateChosen.set(Calendar.YEAR, year) - } - - override fun saveCallback() { - Timber.d("Log: saveCallback: Started") - if(recyclerUpdateView == null) { - Timber.e("Log: saveCallback: RecyclerUpdateView is null, cannot update recycler") - } else { - recyclerUpdateView!!.fillData() - } - Toast.makeText(activity, resources.getString(R.string.item_added), Toast.LENGTH_SHORT).show() - dismiss() - } - - override fun displayImage(imagePath: String) { - Timber.d("Log: displayImage: Started with imagePath = $imagePath") - + private fun displayImage(imagePath: String) { if(imagePath.isBlank()) { Glide.with(this) .load(resources.getDrawable(R.drawable.ic_add_a_photo_24px, null)) @@ -370,14 +263,4 @@ class AddItemBottomSheet : RoundedBottomDialogFragment(), AddItemView { .into(iconImageView) } } - - override fun displayEmptyToast() { - Timber.d("Log: displayEmptyToast: Started") - Toast.makeText(activity, resources.getString(R.string.empty), Toast.LENGTH_SHORT).show() - } - - override fun displayErrorToast() { - Timber.d("Log: displayErrorToast: Started") - Toast.makeText(activity, resources.getString(R.string.error), Toast.LENGTH_SHORT).show() - } } \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemModel.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemModel.kt deleted file mode 100644 index 28f1c8e..0000000 --- a/app/src/main/java/app/marcdev/earworm/additem/AddItemModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.marcdev.earworm.additem - -import app.marcdev.earworm.database.FavouriteItem -import java.io.File - -interface AddItemModel { - - fun addItemAsync(item: FavouriteItem) - - fun getItemAsync(itemId: Int) - - fun saveFileToAppStorage(file: File) - - fun countUsesOfImage(filePath: String) - - fun deleteImage(filePath: String) -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemModelImpl.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemModelImpl.kt deleted file mode 100644 index 75a5600..0000000 --- a/app/src/main/java/app/marcdev/earworm/additem/AddItemModelImpl.kt +++ /dev/null @@ -1,95 +0,0 @@ -package app.marcdev.earworm.additem - -import android.content.Context -import app.marcdev.earworm.database.AppDatabase -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.repository.FavouriteItemRepository -import app.marcdev.earworm.repository.FavouriteItemRepositoryImpl -import app.marcdev.earworm.utils.getArtworkDirectory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import timber.log.Timber -import java.io.File - -class AddItemModelImpl(private val presenter: AddItemPresenter, private val context: Context) : AddItemModel { - - private var repository: FavouriteItemRepository - - init { - val db: AppDatabase = AppDatabase.getDatabase(context) - repository = FavouriteItemRepositoryImpl(db.dao()) - } - - override fun addItemAsync(item: FavouriteItem) { - Timber.d("Log: addItemAsync: Started") - - GlobalScope.launch(Dispatchers.Main) { - async(Dispatchers.IO) { - repository.insertOrUpdateItem(item) - }.await() - - presenter.addItemCallback() - } - } - - override fun getItemAsync(itemId: Int) { - Timber.d("Log: getItemAsync: Started") - - GlobalScope.launch(Dispatchers.Main) { - val item = async(Dispatchers.IO) { - repository.getItem(itemId) - }.await() - - presenter.getItemCallback(item) - } - } - - override fun saveFileToAppStorage(file: File) { - Timber.d("Log: saveFileToAppStorage: Started") - val toPath = getArtworkDirectory(context) + file.name - Timber.d("Log: saveFileToAppStorage: fileName = ${file.name}") - val toFile = File(toPath) - if(toFile.compareTo(file) != 0) { - Timber.d("Log: saveFileToAppStorage: External file, saving") - try { - file.copyTo(toFile, true) - presenter.saveFileToAppStorageCallback(file.name, null) - } catch(e: NoSuchFileException) { - presenter.saveFileToAppStorageCallback("", e) - Timber.e("Log: saveFileToAppStorage: $e") - } - } else { - Timber.d("Log: saveFileToAppStorage: Internal file of same path, not saving") - presenter.saveFileToAppStorageCallback(file.name, null) - } - } - - override fun countUsesOfImage(filePath: String) { - Timber.d("Log: countUsesOfImage: Started with filePath = $filePath") - val file = File(filePath) - val fileName = file.name - - GlobalScope.launch(Dispatchers.Main) { - val uses = async(Dispatchers.IO) { - repository.countUsesOfImage(fileName) - }.await() - - presenter.countUsesOfImageCallback(filePath, uses) - } - } - - override fun deleteImage(filePath: String) { - Timber.d("Log: deleteImage: Started with filePath = $filePath") - - val file = File(filePath) - if(file.exists()) { - Timber.d("Log: deleteImage: File exists, deleting") - val deletionStatus = file.delete() - Timber.d("Log: deleteImage: Deletion: $deletionStatus") - } else { - Timber.w("Log: deleteImage: File doesn't exist") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenter.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenter.kt deleted file mode 100644 index 3361d06..0000000 --- a/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenter.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.marcdev.earworm.additem - -import app.marcdev.earworm.database.FavouriteItem -import java.util.* - -interface AddItemPresenter { - - fun addItem(primaryInput: String, secondaryInput: String, type: Int, dateChosen: Calendar, itemId: Int?) - - fun addItemCallback() - - fun getItem(itemId: Int) - - fun getItemCallback(items: MutableList) - - fun saveFileToAppStorageCallback(fileName: String, exception: NoSuchFileException?) - - fun updateFilePath(filePath: String) - - fun countUsesOfImageCallback(filePath: String, uses: Int) -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenterImpl.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenterImpl.kt deleted file mode 100644 index d7ff254..0000000 --- a/app/src/main/java/app/marcdev/earworm/additem/AddItemPresenterImpl.kt +++ /dev/null @@ -1,119 +0,0 @@ -package app.marcdev.earworm.additem - -import android.content.Context -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.utils.ALBUM -import app.marcdev.earworm.utils.ARTIST -import app.marcdev.earworm.utils.SONG -import app.marcdev.earworm.utils.getArtworkDirectory -import timber.log.Timber -import java.io.File -import java.util.* - -class AddItemPresenterImpl(private val view: AddItemView, private val context: Context) : AddItemPresenter { - - private val model: AddItemModel - private var imageFilePath: String = "" - private var oldImageFilePath: String = "" - - init { - model = AddItemModelImpl(this, context) - } - - override fun addItem(primaryInput: String, secondaryInput: String, type: Int, dateChosen: Calendar, itemId: Int?) { - Timber.d("Log: addItem: Started") - - if(primaryInput.isBlank() || secondaryInput.isBlank()) { - Timber.d("Log: addItem: Empty input") - view.displayEmptyToast() - } else { - Timber.d("Log: addItem: Adding item") - val day = dateChosen.get(Calendar.DAY_OF_MONTH) - val month = dateChosen.get(Calendar.MONTH) - val year = dateChosen.get(Calendar.YEAR) - - var imageName = "" - - if(imageFilePath.isNotBlank()) { - Timber.d("Log: addItem: imageFilePath = $imageFilePath") - - val imageFile = File(imageFilePath) - imageName = imageFile.name - model.saveFileToAppStorage(imageFile) - } - - val item: FavouriteItem = when(type) { - SONG -> FavouriteItem(primaryInput, "", secondaryInput, "", day, month, year, type, imageName) - ALBUM -> FavouriteItem("", primaryInput, secondaryInput, "", day, month, year, type, imageName) - ARTIST -> FavouriteItem("", "", primaryInput, secondaryInput, day, month, year, type, imageName) - else -> FavouriteItem("", "", "", "", 0, 0, 0, type, imageName) - } - - if(itemId != null) { - Timber.d("Log: addItem: Updating old item with ID = $itemId") - item.id = itemId - } - - model.addItemAsync(item) - } - } - - override fun addItemCallback() { - Timber.d("Log: addItemCallback: Started") - view.saveCallback() - } - - override fun getItem(itemId: Int) { - Timber.d("Log: getItem: Started") - model.getItemAsync(itemId) - } - - override fun getItemCallback(items: MutableList) { - Timber.d("Log: getItemCallback: Started") - - if(items.isNotEmpty()) { - view.convertToEditMode(items.first()) - if(items.first().imageName.isNotBlank()) { - view.displayImage(getArtworkDirectory(context) + items.first().imageName) - } - } else { - Timber.e("Log: getItemCallback: Returned empty list") - } - } - - override fun saveFileToAppStorageCallback(fileName: String, exception: NoSuchFileException?) { - Timber.d("Log: saveFileToAppStorageCallback: Started") - if(exception != null) { - view.displayErrorToast() - } - } - - override fun updateFilePath(filePath: String) { - Timber.d("Log: updateFilePath: Started") - - this.oldImageFilePath = imageFilePath - this.imageFilePath = filePath - Timber.d("Log: updateFilePath: oldImageFilePath = $oldImageFilePath") - Timber.d("Log: updateFilePath: imageFilePath = $imageFilePath") - - if(oldImageFilePath.isNotBlank()) { - model.countUsesOfImage(oldImageFilePath) - } - - if(imageFilePath.isBlank()) { - view.displayImage("") - model.countUsesOfImage(oldImageFilePath) - } - } - - override fun countUsesOfImageCallback(filePath: String, uses: Int) { - Timber.d("Log: countUsesOfImageCallback: Started with filePath = $filePath and uses = $uses") - - if(uses <= 1) { - Timber.d("Log: countUsesOfImageCallback: No other uses, deleting image at $filePath") - model.deleteImage(filePath) - } else { - Timber.d("Log: countUsesOfImageCallback: Used elsewhere, not deleting") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemView.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemView.kt deleted file mode 100644 index aa06a9d..0000000 --- a/app/src/main/java/app/marcdev/earworm/additem/AddItemView.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.marcdev.earworm.additem - -import app.marcdev.earworm.database.FavouriteItem - -interface AddItemView { - - fun saveCallback() - - fun displayEmptyToast() - - fun convertToEditMode(item: FavouriteItem) - - fun displayErrorToast() - - fun displayImage(imagePath: String) -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemViewModel.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemViewModel.kt new file mode 100644 index 0000000..29e6d62 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemViewModel.kt @@ -0,0 +1,206 @@ +package app.marcdev.earworm.additem + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.data.repository.FavouriteItemRepository +import app.marcdev.earworm.internal.ALBUM +import app.marcdev.earworm.internal.ARTIST +import app.marcdev.earworm.internal.SONG +import app.marcdev.earworm.utils.FileUtils +import app.marcdev.earworm.utils.formatDateForDisplay +import kotlinx.coroutines.launch +import java.io.File +import java.util.* + +class AddItemViewModel(private val repository: FavouriteItemRepository, + private val fileUtils: FileUtils) + : ViewModel() { + + private var itemId = -1 + private var _date = Calendar.getInstance() + val date: Calendar + get() = _date + + private val _displayEmptyError = MutableLiveData() + val displayEmpty: LiveData + get() = _displayEmptyError + + private val _displayError = MutableLiveData() + val displayError: LiveData + get() = _displayError + + private val _displayAdded = MutableLiveData() + val displayAdded: LiveData + get() = _displayAdded + + private val _selectedType = MutableLiveData() + val selectedType: LiveData + get() = _selectedType + + private val _dateDisplay = MutableLiveData() + val dateDisplay: LiveData + get() = _dateDisplay + + private val _primaryInputDisplay = MutableLiveData() + val primaryInputDisplay: LiveData + get() = _primaryInputDisplay + + private val _secondaryInputDisplay = MutableLiveData() + val secondaryInputDisplay: LiveData + get() = _secondaryInputDisplay + + private var oldImageName = "" + private var currentImageName = "" + private var newImagePath = "" + private val _displayImage = MutableLiveData() + val displayImage: LiveData + get() = _displayImage + + private val _dismiss = MutableLiveData() + val dismiss: LiveData + get() = _dismiss + + fun passArguments(itemIdArg: Int) { + itemId = itemIdArg + if(itemId != -1) { + convertToEditMode() + } else { + setDefaults() + } + } + + private fun convertToEditMode() { + viewModelScope.launch { + val item = repository.getItem(itemId) + _selectedType.value = item.type + setDate(item.day, item.month, item.year) + setInputs(item) + if(item.imageName.isNotBlank()) { + currentImageName = item.imageName + _displayImage.value = fileUtils.artworkDirectory + item.imageName + } + } + } + + private fun setDefaults() { + _selectedType.value = SONG + getDateDisplay() + } + + fun save(primaryInput: String, secondaryInput: String) { + if(primaryInput.isBlank() || secondaryInput.isBlank()) { + _displayEmptyError.value = true + } else { + val day = _date.get(Calendar.DAY_OF_MONTH) + val month = _date.get(Calendar.MONTH) + val year = _date.get(Calendar.YEAR) + + val imageNameToSave = if(newImagePath.isBlank()) + currentImageName + else { + saveNewImage() + File(newImagePath).name + } + + if(oldImageName.isNotBlank()) + deleteOldImageIfNecessary() + + selectedType.value?.let { type -> + val item: FavouriteItem = when(type) { + SONG -> FavouriteItem(primaryInput, "", secondaryInput, "", day, month, year, type, imageNameToSave) + ALBUM -> FavouriteItem("", primaryInput, secondaryInput, "", day, month, year, type, imageNameToSave) + ARTIST -> FavouriteItem("", "", primaryInput, secondaryInput, day, month, year, type, imageNameToSave) + else -> FavouriteItem("", "", "", "", 0, 0, 0, type, imageNameToSave) + } + + if(itemId != -1) { + item.id = itemId + } + + viewModelScope.launch { + repository.addItem(item) + _displayAdded.value = true + _dismiss.value = true + } + } + } + } + + fun setDate(day: Int, month: Int, year: Int) { + _date.set(Calendar.DAY_OF_MONTH, day) + _date.set(Calendar.MONTH, month) + _date.set(Calendar.YEAR, year) + getDateDisplay() + } + + fun setNewImage(filePath: String) { + newImagePath = filePath + _displayImage.value = filePath + } + + private fun saveNewImage() { + fileUtils.saveImage(File(newImagePath)) + } + + private fun deleteOldImageIfNecessary() { + viewModelScope.launch { + if(oldImageName.isNotBlank()) { + val uses = repository.countUsesOfImage(oldImageName) + if(uses <= 1) { + fileUtils.deleteImage(oldImageName) + } + } + } + } + + fun removeImage() { + if(currentImageName.isNotBlank()) { + oldImageName = currentImageName + currentImageName = "" + _displayImage.value = "" + } + if(newImagePath.isNotBlank()) { + newImagePath = "" + _displayImage.value = "" + } + } + + fun setType(type: Int) { + if(_selectedType.value != type) + _selectedType.value = type + } + + private fun setInputs(item: FavouriteItem) { + when(item.type) { + SONG -> { + _primaryInputDisplay.value = item.songName + _secondaryInputDisplay.value = item.artistName + } + ALBUM -> { + _primaryInputDisplay.value = item.albumName + _secondaryInputDisplay.value = item.artistName + } + ARTIST -> { + _primaryInputDisplay.value = item.artistName + _secondaryInputDisplay.value = item.genre + } + } + } + + private fun getDateDisplay() { + val day = _date.get(Calendar.DAY_OF_MONTH) + val month = _date.get(Calendar.MONTH) + val year = _date.get(Calendar.YEAR) + + val today = Calendar.getInstance() + val displayValue = if(day == today.get(Calendar.DAY_OF_MONTH) && month == today.get(Calendar.MONTH) && year == today.get(Calendar.YEAR)) { + "" + } else { + formatDateForDisplay(_date) + } + _dateDisplay.value = displayValue + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/AddItemViewModelFactory.kt b/app/src/main/java/app/marcdev/earworm/additem/AddItemViewModelFactory.kt new file mode 100644 index 0000000..fa76306 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/additem/AddItemViewModelFactory.kt @@ -0,0 +1,15 @@ +package app.marcdev.earworm.additem + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import app.marcdev.earworm.data.repository.FavouriteItemRepository +import app.marcdev.earworm.utils.FileUtils + +class AddItemViewModelFactory(private val favouriteItemRepository: FavouriteItemRepository, private val fileUtils: FileUtils) + : ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return AddItemViewModel(favouriteItemRepository, fileUtils) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/additem/RecyclerUpdateView.kt b/app/src/main/java/app/marcdev/earworm/additem/RecyclerUpdateView.kt deleted file mode 100644 index 55045bc..0000000 --- a/app/src/main/java/app/marcdev/earworm/additem/RecyclerUpdateView.kt +++ /dev/null @@ -1,5 +0,0 @@ -package app.marcdev.earworm.additem - -interface RecyclerUpdateView { - fun fillData() -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/data/database/AppDatabase.kt b/app/src/main/java/app/marcdev/earworm/data/database/AppDatabase.kt new file mode 100644 index 0000000..bf21434 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/data/database/AppDatabase.kt @@ -0,0 +1,5 @@ +package app.marcdev.earworm.data.database + +interface AppDatabase { + fun dao(): DAO +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/data/database/DAO.kt b/app/src/main/java/app/marcdev/earworm/data/database/DAO.kt new file mode 100644 index 0000000..af1708f --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/data/database/DAO.kt @@ -0,0 +1,26 @@ +package app.marcdev.earworm.data.database + +import androidx.lifecycle.LiveData +import androidx.room.* + +@Dao +interface DAO { + + @Insert(onConflict = OnConflictStrategy.FAIL) + fun insertItem(item: FavouriteItem) + + @Update + fun updateItem(item: FavouriteItem) + + @Query("SELECT * FROM FavouriteItems ORDER BY year DESC, month DESC, day DESC, id DESC") + fun getAllItems(): LiveData> + + @Query("SELECT * FROM FavouriteItems where id = :id") + fun getItemById(id: Int): FavouriteItem + + @Query("DELETE FROM FavouriteItems where id = :id") + fun deleteItemById(id: Int) + + @Query("SELECT COUNT(*) FROM FavouriteItems WHERE imageName = :imageName") + fun getNumberOfEntriesUsingImage(imageName: String): Int +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/database/FavouriteItem.kt b/app/src/main/java/app/marcdev/earworm/data/database/FavouriteItem.kt similarity index 76% rename from app/src/main/java/app/marcdev/earworm/database/FavouriteItem.kt rename to app/src/main/java/app/marcdev/earworm/data/database/FavouriteItem.kt index 45fb289..cecacb4 100644 --- a/app/src/main/java/app/marcdev/earworm/database/FavouriteItem.kt +++ b/app/src/main/java/app/marcdev/earworm/data/database/FavouriteItem.kt @@ -1,9 +1,9 @@ -package app.marcdev.earworm.database +package app.marcdev.earworm.data.database import androidx.room.Entity import androidx.room.PrimaryKey -@Entity(tableName = "favourite_items") +@Entity(tableName = "FavouriteItems") data class FavouriteItem( var songName: String, var albumName: String, @@ -17,5 +17,5 @@ data class FavouriteItem( ) { @PrimaryKey(autoGenerate = true) - var id: Int? = null + var id: Int = 0 } \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/data/database/ProductionAppDatabase.kt b/app/src/main/java/app/marcdev/earworm/data/database/ProductionAppDatabase.kt new file mode 100644 index 0000000..77f9537 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/data/database/ProductionAppDatabase.kt @@ -0,0 +1,54 @@ +package app.marcdev.earworm.data.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +@Database(entities = [FavouriteItem::class], version = 5) +abstract class ProductionAppDatabase : RoomDatabase(), AppDatabase { + abstract override fun dao(): DAO + + companion object { + @Volatile + private var INSTANCE: ProductionAppDatabase? = null + private val LOCK = Any() + + operator fun invoke(context: Context) = INSTANCE + ?: synchronized(LOCK) { + INSTANCE + ?: buildDatabase(context).also { INSTANCE = it } + } + + private fun buildDatabase(context: Context) = + Room.databaseBuilder( + context.applicationContext, + ProductionAppDatabase::class.java, + "AppDatabase.db" + ) + .setJournalMode(JournalMode.TRUNCATE) + .addMigrations(MIGRATION_4_TO_5()) + .build() + } + + class MIGRATION_4_TO_5 : Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE favourite_items RENAME TO favourite_items_old") + database.execSQL("CREATE TABLE FavouriteItems(" + + "id INTEGER PRIMARY KEY NOT NULL," + + "songName TEXT NOT NULL," + + "albumName TEXT NOT NULL," + + "artistName TEXT NOT NULL," + + "genre TEXT NOT NULL," + + "day INTEGER NOT NULL," + + "month INTEGER NOT NULL," + + "year INTEGER NOT NULL," + + "type INTEGER NOT NULL," + + "imageName TEXT NOT NULL)") + database.execSQL("INSERT INTO FavouriteItems SELECT * FROM favourite_items_old") + database.execSQL("DROP TABLE favourite_items_old") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/data/repository/FavouriteItemRepository.kt b/app/src/main/java/app/marcdev/earworm/data/repository/FavouriteItemRepository.kt new file mode 100644 index 0000000..ae66852 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/data/repository/FavouriteItemRepository.kt @@ -0,0 +1,17 @@ +package app.marcdev.earworm.data.repository + +import androidx.lifecycle.LiveData +import app.marcdev.earworm.data.database.FavouriteItem + +interface FavouriteItemRepository { + + val allItems: LiveData> + + suspend fun addItem(item: FavouriteItem) + + suspend fun getItem(id: Int): FavouriteItem + + suspend fun deleteItem(id: Int) + + suspend fun countUsesOfImage(imageName: String): Int +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/data/repository/FavouriteItemRepositoryImpl.kt b/app/src/main/java/app/marcdev/earworm/data/repository/FavouriteItemRepositoryImpl.kt new file mode 100644 index 0000000..49d235b --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/data/repository/FavouriteItemRepositoryImpl.kt @@ -0,0 +1,56 @@ +package app.marcdev.earworm.data.repository + +import android.database.sqlite.SQLiteConstraintException +import androidx.lifecycle.LiveData +import app.marcdev.earworm.data.database.DAO +import app.marcdev.earworm.data.database.FavouriteItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber + +class FavouriteItemRepositoryImpl(private val dao: DAO) : FavouriteItemRepository { + + override val allItems: LiveData> by lazy { + dao.getAllItems() + } + + override suspend fun addItem(item: FavouriteItem) { + withContext(Dispatchers.IO) { + try { + Timber.d("Log: addItem: Item doesn't exist, adding new") + dao.insertItem(item) + } catch(exception: SQLiteConstraintException) { + Timber.d("Log: addItem: Item already exists, updating existing") + dao.updateItem(item) + } + } + } + + + override suspend fun getItem(id: Int): FavouriteItem { + return withContext(Dispatchers.IO) { + return@withContext dao.getItemById(id) + } + } + + override suspend fun deleteItem(id: Int) { + withContext(Dispatchers.IO) { + dao.deleteItemById(id) + } + } + + override suspend fun countUsesOfImage(imageName: String): Int { + return withContext(Dispatchers.IO) { + return@withContext dao.getNumberOfEntriesUsingImage(imageName) + } + } + + companion object { + @Volatile private var instance: FavouriteItemRepositoryImpl? = null + + fun getInstance(dao: DAO) = + instance ?: synchronized(this) { + instance ?: FavouriteItemRepositoryImpl(dao).also { instance = it } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/database/AppDatabase.kt b/app/src/main/java/app/marcdev/earworm/database/AppDatabase.kt deleted file mode 100644 index 3cb8774..0000000 --- a/app/src/main/java/app/marcdev/earworm/database/AppDatabase.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.marcdev.earworm.database - -import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase - -@Database(entities = [FavouriteItem::class], version = 4) -abstract class AppDatabase : RoomDatabase() { - - abstract fun dao(): DAO - - companion object { - @Volatile - private var INSTANCE: AppDatabase? = null - - fun getDatabase(context: Context): AppDatabase { - val tempInstance = INSTANCE - - if(tempInstance != null) { - return tempInstance - } - - synchronized(this) { - - val instance = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "AppDatabase.db") - .fallbackToDestructiveMigration().build() - - INSTANCE = instance - return instance - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/database/DAO.kt b/app/src/main/java/app/marcdev/earworm/database/DAO.kt deleted file mode 100644 index b799ce7..0000000 --- a/app/src/main/java/app/marcdev/earworm/database/DAO.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.marcdev.earworm.database - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query - -@Dao -interface DAO { - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertOrUpdateItem(item: FavouriteItem) - - @Query("SELECT * FROM favourite_items") - fun getAllItems(): MutableList - - @Query("SELECT * FROM favourite_items where id = :id") - fun getItemById(id: Int): MutableList - - @Query("DELETE FROM favourite_items where id = :id") - fun deleteItemById(id: Int) - - @Query("SELECT COUNT(*) FROM favourite_items WHERE imageName = :imageName") - fun getNumberOfEntriesUsingImage(imageName: String): Int -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/internal/Constants.kt b/app/src/main/java/app/marcdev/earworm/internal/Constants.kt new file mode 100644 index 0000000..7190ff7 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/internal/Constants.kt @@ -0,0 +1,27 @@ +package app.marcdev.earworm.internal + +import app.marcdev.earworm.utils.ItemFilter + +val DEFAULT_FILTER = ItemFilter(1, 0, 1900, 31, 11, 2099, true, true, true, "") + +// +const val SONG = 0 +const val ALBUM = 1 +const val ARTIST = 2 +const val GENRE = 3 +const val HEADER = 4 +// + +// +const val LIGHT_THEME = 0 +const val DARK_THEME = 1 +// + +// +const val PREF_THEME = "pref_theme" +const val PREF_SHOW_TIPS = "pref_show_tips" +const val PREF_BUILD_NUMBER = "pref_build_number" +const val PREF_LICENSES = "pref_licenses" +const val PREF_GITHUB = "pref_github" +const val PREF_CLEAR_INPUTS = "pref_clear_inputs_on_type_change" +// \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/internal/base/EarwormActivity.kt b/app/src/main/java/app/marcdev/earworm/internal/base/EarwormActivity.kt new file mode 100644 index 0000000..b279f0f --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/internal/base/EarwormActivity.kt @@ -0,0 +1,19 @@ +package app.marcdev.earworm.internal.base + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager + +abstract class EarwormActivity : AppCompatActivity() { + /** + * Replaces a fragment in a frame with another fragment + * @param fragment The fragment to display + * @param fragmentManager The Fragment Manager + * @param frameId The ID of the frame to display the new fragment in + */ + protected fun setFragment(fragment: Fragment, fragmentManager: FragmentManager, frameId: Int) { + val fragmentTransaction = fragmentManager.beginTransaction() + fragmentTransaction.replace(frameId, fragment) + fragmentTransaction.commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/uicomponents/RoundedBottomDialogFragment.kt b/app/src/main/java/app/marcdev/earworm/internal/base/EarwormBottomSheetDialogFragment.kt similarity index 77% rename from app/src/main/java/app/marcdev/earworm/uicomponents/RoundedBottomDialogFragment.kt rename to app/src/main/java/app/marcdev/earworm/internal/base/EarwormBottomSheetDialogFragment.kt index f8eb9c1..9f00774 100644 --- a/app/src/main/java/app/marcdev/earworm/uicomponents/RoundedBottomDialogFragment.kt +++ b/app/src/main/java/app/marcdev/earworm/internal/base/EarwormBottomSheetDialogFragment.kt @@ -1,4 +1,4 @@ -package app.marcdev.earworm.uicomponents +package app.marcdev.earworm.internal.base import android.app.Dialog import android.os.Bundle @@ -6,7 +6,7 @@ import app.marcdev.earworm.R import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -open class RoundedBottomDialogFragment : BottomSheetDialogFragment() { +abstract class EarwormBottomSheetDialogFragment : BottomSheetDialogFragment() { override fun getTheme(): Int { return R.style.Earworm_BottomSheetDialogTheme diff --git a/app/src/main/java/app/marcdev/earworm/internal/base/EarwormDialogFragment.kt b/app/src/main/java/app/marcdev/earworm/internal/base/EarwormDialogFragment.kt new file mode 100644 index 0000000..d6b545e --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/internal/base/EarwormDialogFragment.kt @@ -0,0 +1,11 @@ +package app.marcdev.earworm.internal.base + +import androidx.fragment.app.DialogFragment +import app.marcdev.earworm.R + +abstract class EarwormDialogFragment: DialogFragment() { + override fun onStart() { + super.onStart() + requireDialog().window?.setBackgroundDrawableResource(R.drawable.rounded_dialog_background) + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewImpl.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragment.kt similarity index 50% rename from app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewImpl.kt rename to app/src/main/java/app/marcdev/earworm/mainscreen/MainFragment.kt index 0024ef0..2fdd8f7 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewImpl.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragment.kt @@ -2,30 +2,51 @@ package app.marcdev.earworm.mainscreen import android.content.Intent import android.os.Bundle +import android.preference.PreferenceManager import android.text.Editable import android.text.TextWatcher import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* +import android.widget.EditText +import android.widget.ImageView +import android.widget.ProgressBar +import android.widget.TextView import androidx.core.widget.NestedScrollView import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import app.marcdev.earworm.R import app.marcdev.earworm.additem.AddItemBottomSheet -import app.marcdev.earworm.additem.RecyclerUpdateView -import app.marcdev.earworm.database.FavouriteItem +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.internal.DARK_THEME +import app.marcdev.earworm.internal.PREF_SHOW_TIPS import app.marcdev.earworm.mainscreen.mainrecycler.MainRecyclerAdapter import app.marcdev.earworm.settingsscreen.SettingsActivity +import app.marcdev.earworm.uicomponents.BinaryOptionDialog import app.marcdev.earworm.uicomponents.FilterDialog -import app.marcdev.earworm.utils.* +import app.marcdev.earworm.utils.ItemFilter +import app.marcdev.earworm.utils.changeColorOfDrawable +import app.marcdev.earworm.utils.changeColorOfImageViewDrawable +import app.marcdev.earworm.utils.getTheme import com.google.android.material.floatingactionbutton.FloatingActionButton -import timber.log.Timber +import com.google.android.material.snackbar.Snackbar +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.closestKodein +import org.kodein.di.generic.instance -class MainFragmentViewImpl : Fragment(), MainFragmentView, RecyclerUpdateView { +class MainFragment : Fragment(), KodeinAware { + override val kodein by closestKodein() + // + private val viewModelFactory: MainFragmentViewModelFactory by instance() + private lateinit var viewModel: MainFragmentViewModel + // + + // private lateinit var fab: FloatingActionButton private lateinit var noEntriesWarning: TextView private lateinit var noEntriesWarningImage: ImageView @@ -33,29 +54,28 @@ class MainFragmentViewImpl : Fragment(), MainFragmentView, RecyclerUpdateView { private lateinit var noFilteredResultsWarningImage: ImageView private lateinit var progressBar: ProgressBar private lateinit var searchInput: EditText - private lateinit var searchButton: ImageButton - private lateinit var filterButton: ImageButton - private lateinit var settingsButton: ImageButton + private lateinit var searchButton: ImageView + private lateinit var filterButton: ImageView + private lateinit var settingsButton: ImageView private lateinit var filterDialog: FilterDialog private lateinit var recyclerAdapter: MainRecyclerAdapter - private lateinit var presenter: MainFragmentPresenter - private var isSearchMode = true + // + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel = ViewModelProviders.of(this, viewModelFactory).get(MainFragmentViewModel::class.java) + } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - Timber.d("Log: onCreateView: Started") val view = inflater.inflate(R.layout.fragment_mainscreen, container, false) - - presenter = MainFragmentPresenterImpl(this, activity!!.applicationContext) bindViews(view) + setupRecycler(view) + setupObservers() if(getTheme(requireContext()) == DARK_THEME) { - Timber.d("Log: onCreateView: Is dark mode, converting") convertToDarkMode() } - setupRecycler(view) - fillData() - // If arguments is not null, see if the app has been opened from an app shortcut arguments?.let { if(arguments!!.getBoolean("add_item", false)) { @@ -67,7 +87,6 @@ class MainFragmentViewImpl : Fragment(), MainFragmentView, RecyclerUpdateView { } private fun bindViews(view: View) { - Timber.v("Log: bindViews: Started") this.fab = view.findViewById(R.id.fab_main) fab.setOnClickListener(fabOnClickListener) @@ -94,21 +113,18 @@ class MainFragmentViewImpl : Fragment(), MainFragmentView, RecyclerUpdateView { val nestedScrollView: NestedScrollView = view.findViewById(R.id.scroll_main) nestedScrollView.setOnScrollChangeListener(scrollViewOnScrollChangeListener) - this.filterDialog = FilterDialog(requireActivity(), presenter) + this.filterDialog = FilterDialog(::filterOkClick) this.settingsButton = view.findViewById(R.id.img_settings) settingsButton.setOnClickListener(settingsOnClickListener) } private val fabOnClickListener = View.OnClickListener { - Timber.d("Log: Fab Clicked") val addDialog = AddItemBottomSheet() - addDialog.bindRecyclerUpdateView(this) - addDialog.show(fragmentManager, "Add Item Bottom Sheet Dialog") + addDialog.show(requireFragmentManager(), "Add Item Bottom Sheet Dialog") } private val settingsOnClickListener = View.OnClickListener { - Timber.d("Log: Settings Clicked") val intent = Intent(requireContext(), SettingsActivity::class.java) startActivity(intent) } @@ -126,32 +142,29 @@ class MainFragmentViewImpl : Fragment(), MainFragmentView, RecyclerUpdateView { override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { if(s.isNullOrBlank()) { - presenter.search("") + viewModel.search("") } } } private fun testIfSubmitButtonClicked(keyEvent: KeyEvent, keyCode: Int): Boolean { - Timber.d("Log: testIfSubmitButtonClicked: Submit button clicked") if((keyEvent.action == KeyEvent.ACTION_DOWN) && keyCode == KeyEvent.KEYCODE_ENTER) { - presenter.search(searchInput.text.toString()) + viewModel.search(searchInput.text.toString()) return true } return false } private val searchOnClickListener = View.OnClickListener { - Timber.d("Log: Search Clicked") - if(isSearchMode) { - presenter.search(searchInput.text.toString()) - } else { - searchInput.setText("") - } + viewModel.search(searchInput.text.toString()) + } + + private val clearSearchClickListener = View.OnClickListener { + searchInput.setText("") } private val filterOnClickListener = View.OnClickListener { - Timber.d("Log: Filter Clicked") - filterDialog.show() + filterDialog.show(requireFragmentManager(), "Filter Dialog") } private var scrollViewOnScrollChangeListener = { _: View, _: Int, scrollY: Int, _: Int, oldScrollY: Int -> hideFabOnScroll(scrollY, oldScrollY) } @@ -164,109 +177,106 @@ class MainFragmentViewImpl : Fragment(), MainFragmentView, RecyclerUpdateView { } } + private fun filterOkClick(filter: ItemFilter) { + viewModel.filter(filter) + } + private fun setupRecycler(view: View) { - Timber.v("Log: setupRecycler: Started") val recycler: RecyclerView = view.findViewById(R.id.recycler_main) - this.recyclerAdapter = MainRecyclerAdapter(context, presenter) + this.recyclerAdapter = MainRecyclerAdapter(requireContext(), ::itemClick, ::itemLongClick) recycler.adapter = recyclerAdapter recycler.layoutManager = LinearLayoutManager(context) } - override fun fillData() { - Timber.d("Log: fillData: Started") - displayProgress(true) - displayNoEntriesWarning(false) - presenter.getAllItems() - } + private fun setupObservers() { + viewModel.displayLoading.observe(this, Observer { value -> + value?.let { show -> + progressBar.visibility = if(show) View.VISIBLE else View.GONE + } + }) - override fun displayNoEntriesWarning(display: Boolean) { - Timber.d("Log: displayNoEntriesWarning: Started with display = $display") + viewModel.displayNoEntries.observe(this, Observer { value -> + value?.let { show -> + noEntriesWarning.visibility = if(show) View.VISIBLE else View.GONE + noEntriesWarningImage.visibility = if(show) View.VISIBLE else View.GONE + } + }) - if(display) { - Timber.d("Log: displayNoEntriesWarning: Displaying") - noEntriesWarning.visibility = View.VISIBLE - noEntriesWarningImage.visibility = View.VISIBLE - } else { - Timber.d("Log: displayNoEntriesWarning: Hiding") - noEntriesWarning.visibility = View.GONE - noEntriesWarningImage.visibility = View.GONE - } - } + viewModel.displayNoFilteredResults.observe(this, Observer { value -> + value?.let { show -> + noFilteredResultsWarning.visibility = if(show) View.VISIBLE else View.GONE + noFilteredResultsWarningImage.visibility = if(show) View.VISIBLE else View.GONE + } + }) - override fun updateRecycler(items: List) { - Timber.d("Log: updateRecycler: Started") - recyclerAdapter.updateItems(items) - } + viewModel.displayData.observe(this, Observer { items -> + items?.let { + recyclerAdapter.updateItems(items) + } + }) - override fun displayAddedToast() { - Timber.d("Log: displayAddedToast: Started") - Toast.makeText(activity, resources.getString(R.string.item_added), Toast.LENGTH_SHORT).show() + viewModel.colorFilterIcon.observe(this, Observer { value -> + value?.let { colorIt -> + changeColorOfImageViewDrawable(context!!, filterButton, colorIt) + } + }) + + viewModel.displaySearchIcon.observe(this, Observer { value -> + value?.let { show -> + if(show) { + searchButton.setImageDrawable(resources.getDrawable(R.drawable.ic_search_24px, null)) + searchButton.setOnClickListener(searchOnClickListener) + } else { + searchButton.setImageDrawable(resources.getDrawable(R.drawable.ic_close_24px, null)) + searchButton.setOnClickListener(clearSearchClickListener) + } + } + }) } - override fun displayItemDeletedToast() { - Timber.d("Log: displayItemDeletedToast: Started") - Toast.makeText(activity, resources.getString(R.string.item_deleted), Toast.LENGTH_SHORT).show() + private fun itemClick() { + val prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + if(prefs.getBoolean(PREF_SHOW_TIPS, true)) { + val snackbar = Snackbar.make(requireView(), resources.getString(R.string.long_click_hint), Snackbar.LENGTH_SHORT) + snackbar.setAction(resources.getString(R.string.dont_show)) { + prefs.edit().putBoolean("pref_show_tips", false).apply() + } + snackbar.show() + } } - override fun displayProgress(isVisible: Boolean) { - Timber.d("Log: displayProgress: Started with isVisible = $isVisible") - if(isVisible) { - progressBar.visibility = View.VISIBLE - } else { - progressBar.visibility = View.GONE - } + private fun itemLongClick(favouriteItem: FavouriteItem) { + val dialogBuilder = BinaryOptionDialog.Builder() + dialogBuilder + .setTitle(resources.getString(R.string.edit_or_delete)) + .setMessageVisible(false) + .setNegativeButton(resources.getString(R.string.delete), { + deleteClick(favouriteItem) + }, true) + .setPositiveButton(resources.getString(R.string.edit), { + editClick(favouriteItem) + }, true) + dialogBuilder.build().show(requireFragmentManager(), "Edit or Delete Dialog") } - override fun displayEditItemSheet(itemId: Int) { - Timber.d("Log: displayEditItemSheet: Started with itemId = $itemId") + private fun editClick(favouriteItem: FavouriteItem) { val addDialog = AddItemBottomSheet() - addDialog.bindRecyclerUpdateView(this) val args = Bundle() - args.putInt("item_id", itemId) + args.putInt("item_id", favouriteItem.id) addDialog.arguments = args - addDialog.show(fragmentManager, "Add Item Bottom Sheet Dialog") - } - - override fun displayNoFilteredResultsWarning(display: Boolean) { - Timber.d("Log: displayNoFilteredResultsWarning: Started with display = $display") - - if(display && (noEntriesWarning.visibility == View.GONE)) { - this.noFilteredResultsWarning.visibility = View.VISIBLE - this.noFilteredResultsWarningImage.visibility = View.VISIBLE - } else { - this.noFilteredResultsWarning.visibility = View.GONE - this.noFilteredResultsWarningImage.visibility = View.GONE - } - } - - override fun getActiveFilter(): ItemFilter { - Timber.d("Log: getActiveFilter: Started") - return filterDialog.activeFilter + addDialog.show(requireFragmentManager(), "Add Item Bottom Sheet Dialog") } - override fun changeSearchIcon(isSearch: Boolean) { - Timber.d("Log: changeSearchIcon: Started") - if(isSearch) { - searchButton.setImageDrawable(resources.getDrawable(R.drawable.ic_search_24px, null)) - this.isSearchMode = true - } else { - searchButton.setImageDrawable(resources.getDrawable(R.drawable.ic_close_24px, null)) - this.isSearchMode = false - } - } - - override fun activateFilterIcon(isActive: Boolean) { - Timber.d("Log: activateFilterIcon: Started with isActive = $isActive") - - changeColorOfImageButtonDrawable(context!!, filterButton, isActive) + private fun deleteClick(favouriteItem: FavouriteItem) { + viewModel.deleteItem(favouriteItem) } private fun convertToDarkMode() { - changeColorOfImageButtonDrawable(requireContext(), filterButton, false) - changeColorOfImageButtonDrawable(requireContext(), settingsButton, false) - changeColorOfImageButtonDrawable(requireContext(), searchButton, false) + changeColorOfImageViewDrawable(requireContext(), filterButton, false) + changeColorOfImageViewDrawable(requireContext(), settingsButton, false) + changeColorOfImageViewDrawable(requireContext(), searchButton, false) changeColorOfDrawable(requireContext(), noEntriesWarningImage.drawable, false) changeColorOfDrawable(requireContext(), noFilteredResultsWarningImage.drawable, false) } diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModel.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModel.kt deleted file mode 100644 index b6ac979..0000000 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModel.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.marcdev.earworm.mainscreen - -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.utils.ItemFilter - -interface MainFragmentModel { - - fun getAllItemsAsync() - - fun getAllItemsAsync(filter: ItemFilter) - - fun deleteItemAsync(item: FavouriteItem) - - fun countUsesOfImage(item: FavouriteItem) - - fun deleteImage(filePath: String) -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModelImpl.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModelImpl.kt deleted file mode 100644 index 27eccdd..0000000 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentModelImpl.kt +++ /dev/null @@ -1,89 +0,0 @@ -package app.marcdev.earworm.mainscreen - -import android.content.Context -import app.marcdev.earworm.database.AppDatabase -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.repository.FavouriteItemRepository -import app.marcdev.earworm.repository.FavouriteItemRepositoryImpl -import app.marcdev.earworm.utils.ItemFilter -import app.marcdev.earworm.utils.getArtworkDirectory -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import timber.log.Timber -import java.io.File - -class MainFragmentModelImpl(private val presenter: MainFragmentPresenter, private val context: Context) : MainFragmentModel { - - private var repository: FavouriteItemRepository - - init { - val db: AppDatabase = AppDatabase.getDatabase(context) - repository = FavouriteItemRepositoryImpl(db.dao()) - } - - override fun getAllItemsAsync() { - Timber.d("Log: getAllItemsAsync: Started") - - GlobalScope.launch(Dispatchers.Main) { - val allItems = async(Dispatchers.IO) { - repository.getAllItems() - }.await() - - presenter.getAllItemsCallback(allItems) - } - } - - override fun getAllItemsAsync(filter: ItemFilter) { - Timber.d("Log: getAllItemsAsync: Started") - - GlobalScope.launch(Dispatchers.Main) { - val allItems = async(Dispatchers.IO) { - repository.getAllItems() - }.await() - - presenter.getAllItemsCallback(allItems, filter) - } - } - - override fun deleteItemAsync(item: FavouriteItem) { - Timber.d("Log: deleteItemAsync: Started") - - GlobalScope.launch(Dispatchers.Main) { - async(Dispatchers.IO) { - repository.deleteItem(item.id!!) - }.await() - - presenter.deleteItemCallback() - } - } - - override fun countUsesOfImage(item: FavouriteItem) { - Timber.d("Log: countUsesOfImage: Started with item = $item") - val filePath = getArtworkDirectory(context) + item.imageName - val file = File(filePath) - val fileName = file.name - - GlobalScope.launch(Dispatchers.Main) { - val uses = async(Dispatchers.IO) { - repository.countUsesOfImage(fileName) - }.await() - - presenter.countUsesOfImageCallback(item, uses) - } - } - - override fun deleteImage(filePath: String) { - Timber.d("Log: deleteImage: Started with filePath = $filePath") - - val file = File(filePath) - if(file.exists()) { - Timber.d("Log: deleteImage: File exists, deleting") - val deletionStatus = file.delete() - Timber.d("Log: deleteImage: Deletion: $deletionStatus") - } else { - Timber.w("Log: deleteImage: File doesn't exist") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenter.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenter.kt deleted file mode 100644 index 8aae525..0000000 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenter.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.marcdev.earworm.mainscreen - -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.utils.ItemFilter - -interface MainFragmentPresenter { - - fun getAllItems() - - fun getAllItems(filter: ItemFilter) - - fun getAllItemsCallback(items: MutableList) - - fun getAllItemsCallback(items: MutableList, filter: ItemFilter) - - fun deleteItem(item: FavouriteItem) - - fun deleteItemCallback() - - fun editItemClick(itemId: Int) - - fun search(input: String) - - fun countUsesOfImageCallback(item: FavouriteItem, uses: Int) -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenterImpl.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenterImpl.kt deleted file mode 100644 index b8cd93c..0000000 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentPresenterImpl.kt +++ /dev/null @@ -1,109 +0,0 @@ -package app.marcdev.earworm.mainscreen - -import android.content.Context -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.utils.* -import timber.log.Timber - -class MainFragmentPresenterImpl(val view: MainFragmentView, val context: Context) : MainFragmentPresenter { - private var model = MainFragmentModelImpl(this, context) - - override fun getAllItems() { - Timber.d("Log: getAllItems: Started") - model.getAllItemsAsync() - } - - override fun getAllItems(filter: ItemFilter) { - Timber.d("Log: getAllItems with Filter: Started") - Timber.i("Log: getAllItems: Input Filter = $filter") - if(filter != DEFAULT_FILTER.copy()) { - view.activateFilterIcon(true) - } else { - view.activateFilterIcon(false) - } - model.getAllItemsAsync(filter) - } - - override fun getAllItemsCallback(items: MutableList) { - Timber.d("Log: getAllItemsCallback: Started") - - val sortedItems = sortByDateDescending(items) - val itemsWithHeaders = addListHeaders(sortedItems) - - view.updateRecycler(itemsWithHeaders) - view.displayProgress(false) - - if(itemsWithHeaders.isEmpty()) { - view.displayNoEntriesWarning(true) - } else { - view.displayNoEntriesWarning(false) - } - } - - override fun getAllItemsCallback(items: MutableList, filter: ItemFilter) { - Timber.d("Log: getAllItemsCallback: Started with filter = $filter") - - val sortedItems = applyFilter(items, filter) - val itemsWithHeaders = addListHeaders(sortedItems) - - view.updateRecycler(itemsWithHeaders) - view.displayProgress(false) - - if(itemsWithHeaders.isEmpty()) { - view.displayNoFilteredResultsWarning(true) - } else { - view.displayNoFilteredResultsWarning(false) - } - } - - override fun deleteItem(item: FavouriteItem) { - Timber.d("Log: deleteItem: Started") - if(item.imageName != "") { - Timber.d("Log: deleteItem: Item has an image, checking if used elsewhere") - model.countUsesOfImage(item) - } else { - Timber.d("Log: deleteItem: Item does not have an image, deleting item") - model.deleteItemAsync(item) - } - } - - override fun deleteItemCallback() { - Timber.d("Log: deleteItemCallback: Started") - if(view.getActiveFilter() != DEFAULT_FILTER.copy()) { - getAllItems(view.getActiveFilter()) - } else { - getAllItems() - } - } - - override fun countUsesOfImageCallback(item: FavouriteItem, uses: Int) { - Timber.d("Log: countUsesOfImageCallback: Started with imageName = ${item.imageName} and uses = $uses") - - if(uses <= 1) { - val filePath = getArtworkDirectory(context) + item.imageName - model.deleteImage(filePath) - } - - model.deleteItemAsync(item) - } - - override fun editItemClick(itemId: Int) { - Timber.d("Log: editItemClick: Started") - view.displayEditItemSheet(itemId) - } - - override fun search(input: String) { - Timber.d("Log: search: Started with input = $input") - if(input.isBlank()) { - val inputFilter = view.getActiveFilter() - inputFilter.searchTerm = "" - model.getAllItemsAsync(inputFilter) - view.changeSearchIcon(true) - } else { - val inputFilter = view.getActiveFilter() - inputFilter.searchTerm = input.trim() - model.getAllItemsAsync(inputFilter) - view.changeSearchIcon(false) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentView.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentView.kt deleted file mode 100644 index 86660e2..0000000 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentView.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.marcdev.earworm.mainscreen - -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.utils.ItemFilter - -interface MainFragmentView { - - fun displayNoEntriesWarning(display: Boolean) - - fun displayNoFilteredResultsWarning(display: Boolean) - - fun displayAddedToast() - - fun displayItemDeletedToast() - - fun updateRecycler(items: List) - - fun displayProgress(isVisible: Boolean) - - fun displayEditItemSheet(itemId: Int) - - fun getActiveFilter(): ItemFilter - - fun changeSearchIcon(isSearch: Boolean) - - fun activateFilterIcon(isActive: Boolean) -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewModel.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewModel.kt new file mode 100644 index 0000000..54026ad --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewModel.kt @@ -0,0 +1,241 @@ +package app.marcdev.earworm.mainscreen + +import androidx.lifecycle.* +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.data.repository.FavouriteItemRepository +import app.marcdev.earworm.internal.* +import app.marcdev.earworm.utils.FileUtils +import app.marcdev.earworm.utils.ItemFilter +import kotlinx.coroutines.launch + +class MainFragmentViewModel(private val repository: FavouriteItemRepository, + private val fileUtils: FileUtils) + : ViewModel() { + + private val activeFilter = MutableLiveData() + private val allItems = repository.allItems + + private val _displayData = MediatorLiveData>() + val displayData: LiveData> + get() = _displayData + + private val _displayLoading = MutableLiveData() + val displayLoading: LiveData + get() = _displayLoading + + private val _displayNoEntries = MutableLiveData() + val displayNoEntries: LiveData + get() = _displayNoEntries + + private val _displayNoFilteredResults = MutableLiveData() + val displayNoFilteredResults: LiveData + get() = _displayNoFilteredResults + + private val _colorFilterIcon = MutableLiveData() + val colorFilterIcon: LiveData + get() = _colorFilterIcon + + private val _displaySearchIcon = MutableLiveData() + val displaySearchIcon: LiveData + get() = _displaySearchIcon + + init { + _displayNoEntries.value = false + _displayNoFilteredResults.value = false + _displayData.addSource(activeFilter) { filter -> + workOutDisplayData(allItems.value, filter) + } + _displayData.addSource(allItems) { items -> + workOutDisplayData(items, activeFilter.value) + } + } + + private fun workOutDisplayData(items: List?, filter: ItemFilter?) { + _displayLoading.value = true + displaySearchIconIfNeeded(filter) + var finalList: List? = null + + items?.let { fullList -> + _displayNoEntries.value = fullList.isEmpty() + + val filteredList = if(filter != null) { + filterResults(fullList, filter) + } else { + fullList + } + finalList = addListHeaders(filteredList) + } + + _displayData.value = finalList?.toList() + _displayLoading.value = false + } + + fun deleteItem(item: FavouriteItem) { + viewModelScope.launch { + repository.deleteItem(item.id) + deleteImageIfNecessary(item.imageName) + } + } + + fun search(searchTermArg: String) { + val newFilter: ItemFilter? = if(activeFilter.value == null) + DEFAULT_FILTER.copy() + else + activeFilter.value + + newFilter?.searchTerm = searchTermArg + activeFilter.value = newFilter + } + + fun filter(filter: ItemFilter) { + val newFilter: ItemFilter? = if(activeFilter.value == null) + DEFAULT_FILTER.copy() + else + activeFilter.value + if(newFilter != null) { + newFilter.endDay = filter.endDay + newFilter.endMonth = filter.endMonth + newFilter.endYear = filter.endYear + newFilter.startDay = filter.startDay + newFilter.startMonth = filter.startMonth + newFilter.startYear = filter.startYear + newFilter.includeAlbums = filter.includeAlbums + newFilter.includeArtists = filter.includeArtists + newFilter.includeSongs = filter.includeSongs + } + activeFilter.value = newFilter + } + + private suspend fun deleteImageIfNecessary(imageName: String) { + if(imageName.isNotBlank()) { + val uses = repository.countUsesOfImage(imageName) + if(uses == 0) { + fileUtils.deleteImage(imageName) + } + } + } + + private fun displaySearchIconIfNeeded(filter: ItemFilter?) { + if(filter == null) { + _displaySearchIcon.value = true + } else { + _displaySearchIcon.value = filter.searchTerm.isBlank() + } + } + + private fun filterResults(allItems: List, filter: ItemFilter): List { + val filteredByDate = filterByDate(allItems, filter) + val filteredByType = filterByType(filteredByDate, filter) + val filteredByText = filterByText(filteredByType, filter) + + _displayNoFilteredResults.value = filteredByText.isEmpty() + colorFilterIconIfNecessary(filter) + return filteredByText + } + + private fun filterByDate(items: List, filter: ItemFilter): List { + val filteredItems = mutableListOf() + + val filterCompleteDateStart = getCompleteDate(filter.startDay, filter.startMonth, filter.startYear) + val filterCompleteDateEnd: Int = getCompleteDate(filter.endDay, filter.endMonth, filter.endYear) + + for(item in items) { + val itemCompleteDate = getCompleteDate(item.day, item.month, item.year) + if(itemCompleteDate in filterCompleteDateStart..filterCompleteDateEnd) { + filteredItems.add(item) + } + } + + return filteredItems + } + + private fun filterByType(items: List, filter: ItemFilter): List { + val filteredItems = mutableListOf() + + for(item in items) { + if(item.type == SONG && filter.includeSongs) + filteredItems.add(item) + + if(item.type == ALBUM && filter.includeAlbums) + filteredItems.add(item) + + if(item.type == ARTIST && filter.includeArtists) + filteredItems.add(item) + } + + return filteredItems + } + + private fun filterByText(items: List, filter: ItemFilter): List { + val filteredItems = mutableListOf() + + for(item in items) { + if((item.albumName.contains(filter.searchTerm, true) + || (item.artistName.contains(filter.searchTerm, true) + || (item.songName.contains(filter.searchTerm, true) + || (item.genre.contains(filter.searchTerm, true))))) + ) { + filteredItems.add(item) + } + } + return filteredItems + } + + private fun getCompleteDate(day: Int, month: Int, year: Int): Int { + val dayTwoDigit = if(day < 10) + "0$day" + else + "$day" + + val monthTwoDigit = if(month < 10) + "0$month" + else + "$month" + + return "$year$monthTwoDigit$dayTwoDigit".toInt() + } + + private fun addListHeaders(items: List): List { + val listWithHeaders = items.toMutableList() + + var lastMonth = 12 + var lastYear = 9999 + if(items.isNotEmpty()) { + lastMonth = items.first().month + 1 + lastYear = items.first().year + } + + val headersToAdd = mutableListOf>() + + for(x in 0 until items.size) { + if(((items[x].month < lastMonth) && (items[x].year == lastYear)) + || (items[x].month > lastMonth) && (items[x].year < lastYear) + || (items[x].year < lastYear) + ) { + val header = FavouriteItem("", "", "", "", 0, items[x].month, items[x].year, HEADER, "") + lastMonth = items[x].month + lastYear = items[x].year + headersToAdd.add(Pair(x, header)) + } + } + + for((add, x) in (0 until headersToAdd.size).withIndex()) { + listWithHeaders.add(headersToAdd[x].first + add, headersToAdd[x].second) + } + + return listWithHeaders + } + + private fun colorFilterIconIfNecessary(filter: ItemFilter) { + val isDefaultFilter = filter.startDay == DEFAULT_FILTER.startDay + && filter.startMonth == DEFAULT_FILTER.startMonth + && filter.startYear == DEFAULT_FILTER.startYear + && filter.endDay == DEFAULT_FILTER.endDay + && filter.endMonth == DEFAULT_FILTER.endMonth + && filter.endYear == DEFAULT_FILTER.endYear + && filter.includeSongs == DEFAULT_FILTER.includeSongs + && filter.includeAlbums == DEFAULT_FILTER.includeAlbums + && filter.includeArtists == DEFAULT_FILTER.includeArtists + _colorFilterIcon.value = !isDefaultFilter + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewModelFactory.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewModelFactory.kt new file mode 100644 index 0000000..4330abf --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/MainFragmentViewModelFactory.kt @@ -0,0 +1,15 @@ +package app.marcdev.earworm.mainscreen + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import app.marcdev.earworm.data.repository.FavouriteItemRepository +import app.marcdev.earworm.utils.FileUtils + +class MainFragmentViewModelFactory(private val favouriteItemRepository: FavouriteItemRepository, private val fileUtils: FileUtils) + : ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return MainFragmentViewModel(favouriteItemRepository, fileUtils) as T + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerAdapter.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerAdapter.kt index 7c6bce7..ef3463c 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerAdapter.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerAdapter.kt @@ -5,54 +5,47 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.mainscreen.MainFragmentPresenter -import app.marcdev.earworm.utils.* -import timber.log.Timber +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.internal.* -class MainRecyclerAdapter(context: Context?, private val presenter: MainFragmentPresenter) : RecyclerView.Adapter(), MainRecyclerView { +class MainRecyclerAdapter(context: Context, + private val itemClick: () -> Unit, + private val itemLongClick: (FavouriteItem) -> Unit) + : RecyclerView.Adapter() { private var items: List = mutableListOf() private var inflater: LayoutInflater = LayoutInflater.from(context) override fun getItemViewType(position: Int): Int { - Timber.v("Log: getItemViewType: Started") return items[position].type } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainRecyclerViewHolder { - Timber.v("Log: onCreateViewHolder: Started") - lateinit var viewHolder: MainRecyclerViewHolder when(viewType) { HEADER -> { - Timber.v("Log: onCreateViewHolder: Type == Header") val view = inflater.inflate(R.layout.item_header, parent, false) viewHolder = MainRecyclerViewHolderHeader(view) } SONG -> { - Timber.v("Log: onCreateViewHolder: Type == Song") val view = inflater.inflate(R.layout.item_mainrecycler_song, parent, false) - viewHolder = MainRecyclerViewHolderSong(view) + viewHolder = MainRecyclerViewHolderSong(view, itemClick, itemLongClick) } ALBUM -> { - Timber.v("Log: onCreateViewHolder: Type == Album") val view = inflater.inflate(R.layout.item_mainrecycler_album, parent, false) - viewHolder = MainRecyclerViewHolderAlbum(view) + viewHolder = MainRecyclerViewHolderAlbum(view, itemClick, itemLongClick) } ARTIST -> { - Timber.v("Log: onCreateViewHolder: Type == Artist") val view = inflater.inflate(R.layout.item_mainrecycler_artist, parent, false) - viewHolder = MainRecyclerViewHolderArtist(view) + viewHolder = MainRecyclerViewHolderArtist(view, itemClick, itemLongClick) } GENRE -> { - Timber.v("Log: onCreateViewHolder: Type == Genre") val view = inflater.inflate(R.layout.item_mainrecycler_genre, parent, false) - viewHolder = MainRecyclerViewHolderGenre(view) + viewHolder = MainRecyclerViewHolderGenre(view, itemClick, itemLongClick) } } @@ -60,18 +53,14 @@ class MainRecyclerAdapter(context: Context?, private val presenter: MainFragment } override fun onBindViewHolder(holder: MainRecyclerViewHolder, position: Int) { - Timber.v("Log: onBindViewHolder: $position") holder.display(items[position]) - holder.bindPresenterAndItem(presenter, items[position]) } override fun getItemCount(): Int { - Timber.v("Log: getItemCount: ${items.size}") return items.size } - override fun updateItems(items: List) { - Timber.d("Log: updateItems: Started") + fun updateItems(items: List) { this.items = items notifyDataSetChanged() } diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerView.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerView.kt deleted file mode 100644 index 27a2735..0000000 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerView.kt +++ /dev/null @@ -1,7 +0,0 @@ -package app.marcdev.earworm.mainscreen.mainrecycler - -import app.marcdev.earworm.database.FavouriteItem - -interface MainRecyclerView { - fun updateItems(items: List) -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolder.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolder.kt index ae2a229..88c96c8 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolder.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolder.kt @@ -1,78 +1,29 @@ package app.marcdev.earworm.mainscreen.mainrecycler -import android.app.Dialog -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.preference.PreferenceManager import android.view.View import androidx.recyclerview.widget.RecyclerView -import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.mainscreen.MainFragmentPresenter -import com.google.android.material.button.MaterialButton -import com.google.android.material.snackbar.Snackbar -import timber.log.Timber +import app.marcdev.earworm.data.database.FavouriteItem -open class MainRecyclerViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { +open class MainRecyclerViewHolder(itemView: View, + private val itemClick: () -> Unit, + private val itemLongClick: (FavouriteItem) -> Unit) + : RecyclerView.ViewHolder(itemView) { - private lateinit var displayedItem: FavouriteItem - private lateinit var presenter: MainFragmentPresenter - private lateinit var editDialog: Dialog - private val prefs = PreferenceManager.getDefaultSharedPreferences(itemView.context) - - private val snackbarActionListener = View.OnClickListener { - Timber.d("Log: snackbarActionListener: Clicked") - prefs.edit().putBoolean("pref_show_tips", false).apply() - } - - private val itemClickListener = View.OnClickListener { - if(prefs.getBoolean("pref_show_tips", true)) { - val snackbar = Snackbar.make(it, itemView.resources.getString(R.string.long_click_hint), Snackbar.LENGTH_SHORT) - snackbar.setAction(itemView.resources.getString(R.string.dont_show), snackbarActionListener) - snackbar.show() - } - } - - private val itemLongClickListener = View.OnLongClickListener { - editDialog.show() - return@OnLongClickListener true - } - - private val editOnClickListener = View.OnClickListener { - Timber.d("Log: editOnClickListener: Clicked") - editDialog.dismiss() - presenter.editItemClick(displayedItem.id!!) - } - - private val deleteOnClickListener = View.OnClickListener { - Timber.d("Log: deleteOnClickListener: Clicked") - presenter.deleteItem(displayedItem) - editDialog.dismiss() - } + protected var displayedItem: FavouriteItem? = null init { - initEditDialog() - itemView.setOnClickListener(itemClickListener) - itemView.setOnLongClickListener(itemLongClickListener) + itemView.setOnClickListener { + itemClick + } + itemView.setOnLongClickListener { + displayedItem?.let { item -> + itemLongClick(item) + } + true + } } open fun display(favouriteItemToDisplay: FavouriteItem) { // TO BE OVERRIDDEN } - - fun bindPresenterAndItem(presenter: MainFragmentPresenter, item: FavouriteItem) { - this.presenter = presenter - this.displayedItem = item - } - - private fun initEditDialog() { - this.editDialog = Dialog(itemView.context) - editDialog.setContentView(R.layout.dialog_edit_or_delete) - editDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - val editButton = editDialog.findViewById(R.id.btn_add_or_delete_edit) - editButton.setOnClickListener(editOnClickListener) - val deleteButton = editDialog.findViewById(R.id.btn_add_or_delete_delete) - deleteButton.setOnClickListener(deleteOnClickListener) - } } \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderAlbum.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderAlbum.kt index b883c48..61a247e 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderAlbum.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderAlbum.kt @@ -4,14 +4,22 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.utils.FileUtils import app.marcdev.earworm.utils.formatDateForDisplay -import app.marcdev.earworm.utils.getArtworkDirectory import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions -import timber.log.Timber +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.closestKodein +import org.kodein.di.generic.instance -class MainRecyclerViewHolderAlbum(itemView: View) : MainRecyclerViewHolder(itemView) { +class MainRecyclerViewHolderAlbum(itemView: View, + itemClick: () -> Unit, + itemLongClick: (FavouriteItem) -> Unit) + : MainRecyclerViewHolder(itemView, itemClick, itemLongClick), KodeinAware { + override val kodein: Kodein by closestKodein(itemView.context) + private val fileUtils: FileUtils by instance() private val albumNameDisplay: TextView = itemView.findViewById(R.id.txt_albumName) private val albumDateDisplay: TextView = itemView.findViewById(R.id.txt_albumDate) @@ -19,23 +27,19 @@ class MainRecyclerViewHolderAlbum(itemView: View) : MainRecyclerViewHolder(itemV private var albumImageDisplay: ImageView = itemView.findViewById(R.id.img_album_icon) override fun display(favouriteItemToDisplay: FavouriteItem) { - Timber.d("Log: display: $favouriteItemToDisplay") + displayedItem = favouriteItemToDisplay albumNameDisplay.text = favouriteItemToDisplay.albumName albumArtistDisplay.text = favouriteItemToDisplay.artistName val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year) albumDateDisplay.text = date if(favouriteItemToDisplay.imageName.isNotBlank()) { - Timber.d("Log: display: ${favouriteItemToDisplay.imageName}") - Glide.with(itemView) - .load(getArtworkDirectory(itemView.context) + favouriteItemToDisplay.imageName) + .load(fileUtils.artworkDirectory + favouriteItemToDisplay.imageName) .apply(RequestOptions().centerCrop()) .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null))) .into(albumImageDisplay) } else { - Timber.d("Log: display: No image to display") - Glide.with(itemView) .load(itemView.resources.getDrawable(R.drawable.ic_album_24px, null)) .apply(RequestOptions().centerCrop()) diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderArtist.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderArtist.kt index d733aee..f617ceb 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderArtist.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderArtist.kt @@ -4,14 +4,22 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.utils.FileUtils import app.marcdev.earworm.utils.formatDateForDisplay -import app.marcdev.earworm.utils.getArtworkDirectory import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions -import timber.log.Timber +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.closestKodein +import org.kodein.di.generic.instance -class MainRecyclerViewHolderArtist(itemView: View) : MainRecyclerViewHolder(itemView) { +class MainRecyclerViewHolderArtist(itemView: View, + itemClick: () -> Unit, + itemLongClick: (FavouriteItem) -> Unit) + : MainRecyclerViewHolder(itemView, itemClick, itemLongClick), KodeinAware { + override val kodein: Kodein by closestKodein(itemView.context) + private val fileUtils: FileUtils by instance() private val artistNameDisplay: TextView = itemView.findViewById(R.id.txt_artistName) private val artistGenreDisplay: TextView = itemView.findViewById(R.id.txt_artistGenre) @@ -19,23 +27,19 @@ class MainRecyclerViewHolderArtist(itemView: View) : MainRecyclerViewHolder(item private val artistImageDisplay: ImageView = itemView.findViewById(R.id.img_artist_icon) override fun display(favouriteItemToDisplay: FavouriteItem) { - Timber.d("Log: display: $favouriteItemToDisplay") + displayedItem = favouriteItemToDisplay artistNameDisplay.text = favouriteItemToDisplay.artistName artistGenreDisplay.text = favouriteItemToDisplay.genre val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year) artistDateDisplay.text = date if(favouriteItemToDisplay.imageName.isNotBlank()) { - Timber.d("Log: display: imageName = ${favouriteItemToDisplay.imageName}") - Glide.with(itemView) - .load(getArtworkDirectory(itemView.context) + favouriteItemToDisplay.imageName) + .load(fileUtils.artworkDirectory + favouriteItemToDisplay.imageName) .apply(RequestOptions().centerCrop()) .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null))) .into(artistImageDisplay) } else { - Timber.d("Log: display: No image to display") - Glide.with(itemView) .load(itemView.resources.getDrawable(R.drawable.ic_person_24px, null)) .apply(RequestOptions().centerCrop()) diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderGenre.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderGenre.kt index b51afc8..9763f6a 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderGenre.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderGenre.kt @@ -3,17 +3,23 @@ package app.marcdev.earworm.mainscreen.mainrecycler import android.view.View import android.widget.TextView import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem +import app.marcdev.earworm.data.database.FavouriteItem import app.marcdev.earworm.utils.formatDateForDisplay -import timber.log.Timber +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.closestKodein -class MainRecyclerViewHolderGenre(itemView: View) : MainRecyclerViewHolder(itemView) { +class MainRecyclerViewHolderGenre(itemView: View, + itemClick: () -> Unit, + itemLongClick: (FavouriteItem) -> Unit) + : MainRecyclerViewHolder(itemView, itemClick, itemLongClick), KodeinAware { + override val kodein: Kodein by closestKodein(itemView.context) private var genreNameDisplay: TextView = itemView.findViewById(R.id.txt_genreName) private var genreDateDisplay: TextView = itemView.findViewById(R.id.txt_genreDate) override fun display(favouriteItemToDisplay: FavouriteItem) { - Timber.d("Log: display: $favouriteItemToDisplay") + displayedItem = favouriteItemToDisplay genreNameDisplay.text = favouriteItemToDisplay.genre val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year) genreDateDisplay.text = date diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderHeader.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderHeader.kt index 19420dc..ee1d48b 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderHeader.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderHeader.kt @@ -3,11 +3,10 @@ package app.marcdev.earworm.mainscreen.mainrecycler import android.view.View import android.widget.TextView import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem -import app.marcdev.earworm.utils.getMonthName -import timber.log.Timber +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.utils.formatDateForHeaderDisplay -open class MainRecyclerViewHolderHeader(itemView: View) : MainRecyclerViewHolder(itemView) { +open class MainRecyclerViewHolderHeader(itemView: View) : MainRecyclerViewHolder(itemView, {}, {}) { private var dateDisplay: TextView = itemView.findViewById(R.id.txt_header_title) @@ -26,8 +25,6 @@ open class MainRecyclerViewHolderHeader(itemView: View) : MainRecyclerViewHolder } override fun display(favouriteItemToDisplay: FavouriteItem) { - Timber.d("Log: display: $favouriteItemToDisplay") - - dateDisplay.text = ("${getMonthName(favouriteItemToDisplay.month, itemView.context)} ${favouriteItemToDisplay.year}") + dateDisplay.text = formatDateForHeaderDisplay(favouriteItemToDisplay.month, favouriteItemToDisplay.year) } } diff --git a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderSong.kt b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderSong.kt index 0611378..565b741 100644 --- a/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderSong.kt +++ b/app/src/main/java/app/marcdev/earworm/mainscreen/mainrecycler/MainRecyclerViewHolderSong.kt @@ -4,14 +4,22 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import app.marcdev.earworm.R -import app.marcdev.earworm.database.FavouriteItem +import app.marcdev.earworm.data.database.FavouriteItem +import app.marcdev.earworm.utils.FileUtils import app.marcdev.earworm.utils.formatDateForDisplay -import app.marcdev.earworm.utils.getArtworkDirectory import com.bumptech.glide.Glide import com.bumptech.glide.request.RequestOptions -import timber.log.Timber +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.closestKodein +import org.kodein.di.generic.instance -class MainRecyclerViewHolderSong(itemView: View) : MainRecyclerViewHolder(itemView) { +class MainRecyclerViewHolderSong(itemView: View, + itemClick: () -> Unit, + itemLongClick: (FavouriteItem) -> Unit) + : MainRecyclerViewHolder(itemView, itemClick, itemLongClick), KodeinAware { + override val kodein: Kodein by closestKodein(itemView.context) + private val fileUtils: FileUtils by instance() private val songNameDisplay: TextView = itemView.findViewById(R.id.txt_songName) private val songDateDisplay: TextView = itemView.findViewById(R.id.txt_songDate) @@ -19,22 +27,19 @@ class MainRecyclerViewHolderSong(itemView: View) : MainRecyclerViewHolder(itemVi private val songImageDisplay: ImageView = itemView.findViewById(R.id.img_song_icon) override fun display(favouriteItemToDisplay: FavouriteItem) { - Timber.d("Log: display: $favouriteItemToDisplay") + displayedItem = favouriteItemToDisplay songNameDisplay.text = favouriteItemToDisplay.songName songArtistDisplay.text = favouriteItemToDisplay.artistName val date = formatDateForDisplay(favouriteItemToDisplay.day, favouriteItemToDisplay.month, favouriteItemToDisplay.year) songDateDisplay.text = date if(favouriteItemToDisplay.imageName.isNotBlank()) { - Timber.d("Log: display: ${favouriteItemToDisplay.imageName}") Glide.with(itemView) - .load(getArtworkDirectory(itemView.context) + favouriteItemToDisplay.imageName) + .load(fileUtils.artworkDirectory + favouriteItemToDisplay.imageName) .apply(RequestOptions().centerCrop()) .apply(RequestOptions().error(itemView.resources.getDrawable(R.drawable.ic_error_24px, null))) .into(songImageDisplay) } else { - Timber.d("Log: display: No image to display") - Glide.with(itemView) .load(itemView.resources.getDrawable(R.drawable.ic_music_note_24px, null)) .apply(RequestOptions().centerCrop()) diff --git a/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepository.kt b/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepository.kt deleted file mode 100644 index 1b9cebe..0000000 --- a/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.marcdev.earworm.repository - -import app.marcdev.earworm.database.FavouriteItem - -interface FavouriteItemRepository { - - suspend fun insertOrUpdateItem(item: FavouriteItem) - - suspend fun getAllItems(): MutableList - - suspend fun getItem(id: Int): MutableList - - suspend fun deleteItem(id: Int) - - suspend fun countUsesOfImage(imageName: String): Int -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepositoryImpl.kt b/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepositoryImpl.kt deleted file mode 100644 index 80407c4..0000000 --- a/app/src/main/java/app/marcdev/earworm/repository/FavouriteItemRepositoryImpl.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.marcdev.earworm.repository - -import app.marcdev.earworm.database.DAO -import app.marcdev.earworm.database.FavouriteItem - -class FavouriteItemRepositoryImpl(private val dao: DAO) : FavouriteItemRepository { - - override suspend fun insertOrUpdateItem(item: FavouriteItem) { - return dao.insertOrUpdateItem(item) - } - - override suspend fun getAllItems(): MutableList { - return dao.getAllItems() - } - - override suspend fun getItem(id: Int): MutableList { - return dao.getItemById(id) - } - - override suspend fun deleteItem(id: Int) { - return dao.deleteItemById(id) - } - - override suspend fun countUsesOfImage(imageName: String): Int { - return dao.getNumberOfEntriesUsingImage(imageName) - } -} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/settingsscreen/LicensesActivity.kt b/app/src/main/java/app/marcdev/earworm/settingsscreen/LicensesActivity.kt index d910e87..8fada0a 100644 --- a/app/src/main/java/app/marcdev/earworm/settingsscreen/LicensesActivity.kt +++ b/app/src/main/java/app/marcdev/earworm/settingsscreen/LicensesActivity.kt @@ -4,33 +4,28 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View -import android.widget.ImageButton +import android.widget.ImageView import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView import app.marcdev.earworm.R -import app.marcdev.earworm.utils.DARK_THEME -import app.marcdev.earworm.utils.changeColorOfImageButtonDrawable +import app.marcdev.earworm.internal.DARK_THEME +import app.marcdev.earworm.internal.base.EarwormActivity +import app.marcdev.earworm.utils.changeColorOfImageViewDrawable import app.marcdev.earworm.utils.getTheme -import timber.log.Timber -class LicensesActivity : AppCompatActivity() { +class LicensesActivity : EarwormActivity() { private var isDarkMode: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { - Timber.d("Log: onCreate: Started") - /* Theme changes must be done before super.onCreate otherwise it will be overridden with the value in the manifest */ - if(getTheme(applicationContext) == DARK_THEME) { - Timber.v("Log: onCreate: Is dark mode") + isDarkMode = if(getTheme(applicationContext) == DARK_THEME) { setTheme(R.style.Earworm_DarkTheme) - isDarkMode = true + true } else { - Timber.v("Log: onCreate: Is not dark mode") setTheme(R.style.Earworm_LightTheme) - isDarkMode = false + false } super.onCreate(savedInstanceState) @@ -40,11 +35,10 @@ class LicensesActivity : AppCompatActivity() { } private fun bindViews() { - Timber.v("Log: bindViews: Started") - val backButton = findViewById(R.id.img_backFromSettings) + val backButton = findViewById(R.id.img_backFromSettings) backButton.setOnClickListener(backOnClickListener) if(isDarkMode) { - changeColorOfImageButtonDrawable(applicationContext, backButton, false) + changeColorOfImageViewDrawable(applicationContext, backButton, false) } val toolbarTitle = findViewById(R.id.txt_settingsToolbarTitle) @@ -68,12 +62,10 @@ class LicensesActivity : AppCompatActivity() { } private val backOnClickListener = View.OnClickListener { - Timber.d("Log: backClick: Started") finish() } private val glideOnClickListener = View.OnClickListener { - Timber.d("Log: glideClick: Started") val uriUrl = Uri.parse("https://github.com/bumptech/glide") val launchBrowser = Intent(Intent.ACTION_VIEW) launchBrowser.data = uriUrl @@ -81,7 +73,6 @@ class LicensesActivity : AppCompatActivity() { } private val timberOnClickListener = View.OnClickListener { - Timber.d("Log: timberClick: Started") val uriUrl = Uri.parse("https://github.com/JakeWharton/timber") val launchBrowser = Intent(Intent.ACTION_VIEW) launchBrowser.data = uriUrl @@ -89,7 +80,6 @@ class LicensesActivity : AppCompatActivity() { } private val materialIconsOnClickListener = View.OnClickListener { - Timber.d("Log: materialIconsClick: Started") val uriUrl = Uri.parse("https://github.com/google/material-design-icons") val launchBrowser = Intent(Intent.ACTION_VIEW) launchBrowser.data = uriUrl @@ -97,7 +87,6 @@ class LicensesActivity : AppCompatActivity() { } private val materialComponentsOnClickListener = View.OnClickListener { - Timber.d("Log: materialComponentsClick: Started") val uriUrl = Uri.parse("https://github.com/material-components/material-components-android") val launchBrowser = Intent(Intent.ACTION_VIEW) launchBrowser.data = uriUrl @@ -105,7 +94,6 @@ class LicensesActivity : AppCompatActivity() { } private val filePickerOnClickListener = View.OnClickListener { - Timber.d("Log: filePickerClick: Started") val uriUrl = Uri.parse("https://github.com/DroidNinja/Android-FilePicker") val launchBrowser = Intent(Intent.ACTION_VIEW) launchBrowser.data = uriUrl diff --git a/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsActivity.kt b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsActivity.kt index d39ae06..2472d6b 100644 --- a/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsActivity.kt +++ b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsActivity.kt @@ -2,25 +2,19 @@ package app.marcdev.earworm.settingsscreen import android.os.Bundle import android.view.View -import android.widget.ImageButton -import androidx.appcompat.app.AppCompatActivity +import android.widget.ImageView import app.marcdev.earworm.R -import app.marcdev.earworm.utils.DARK_THEME -import app.marcdev.earworm.utils.changeColorOfImageButtonDrawable +import app.marcdev.earworm.internal.DARK_THEME +import app.marcdev.earworm.internal.base.EarwormActivity +import app.marcdev.earworm.utils.changeColorOfImageViewDrawable import app.marcdev.earworm.utils.getTheme -import app.marcdev.earworm.utils.setFragment -import timber.log.Timber -class SettingsActivity : AppCompatActivity() { +class SettingsActivity : EarwormActivity() { override fun onCreate(savedInstanceState: Bundle?) { - Timber.v("Log: onCreate: Started") - if(getTheme(applicationContext) == DARK_THEME) { - Timber.v("Log: onCreate: Is dark mode") setTheme(R.style.Earworm_DarkTheme) } else { - Timber.v("Log: onCreate: Is not dark mode") setTheme(R.style.Earworm_LightTheme) } @@ -32,18 +26,15 @@ class SettingsActivity : AppCompatActivity() { } private fun bindViews() { - Timber.v("Log: bindViews: Started") - val backButton = findViewById(R.id.img_backFromSettings) + val backButton = findViewById(R.id.img_backFromSettings) backButton.setOnClickListener(backOnClickListener) if(getTheme(applicationContext) == DARK_THEME) { - Timber.v("Log: bindViews: Converting to dark mode") - changeColorOfImageButtonDrawable(applicationContext, backButton, false) + changeColorOfImageViewDrawable(applicationContext, backButton, false) } } private val backOnClickListener = View.OnClickListener { - Timber.d("Log: BackClick: Started") this.finish() } } \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsFragment.kt b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsFragment.kt index ecc7014..a98306a 100644 --- a/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsFragment.kt +++ b/app/src/main/java/app/marcdev/earworm/settingsscreen/SettingsFragment.kt @@ -11,7 +11,8 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import app.marcdev.earworm.BuildConfig import app.marcdev.earworm.R -import app.marcdev.earworm.utils.* +import app.marcdev.earworm.internal.* +import app.marcdev.earworm.utils.changeColorOfDrawable import timber.log.Timber class SettingsFragment : PreferenceFragmentCompat() { @@ -52,41 +53,35 @@ class SettingsFragment : PreferenceFragmentCompat() { } private val themeChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> - Timber.d("Log: themeChangeListener: Theme changed to $newValue") requireActivity().recreate() matchSummaryToSelection(preference, newValue.toString()) true } private val clearInputsChangeListener = Preference.OnPreferenceChangeListener { preference, newValue -> - Timber.d("Log: clearInputsChangeListener: Value changed to $newValue") matchSummaryToSelection(preference, newValue.toString()) true } private val resetTipsListener = Preference.OnPreferenceClickListener { - Timber.d("Log: ResetTipsClick: Clicked") prefs.edit().putBoolean(PREF_SHOW_TIPS, true).apply() Toast.makeText(requireContext(), resources.getString(R.string.reset_tips_confirmation), Toast.LENGTH_LONG).show() return@OnPreferenceClickListener true } private val versionClickListener = Preference.OnPreferenceClickListener { - Timber.d("Log: versionClick: Started") val versionCodeString = resources.getString(R.string.build_code) Toast.makeText(requireContext(), "$versionCodeString: ${BuildConfig.VERSION_CODE}", Toast.LENGTH_SHORT).show() true } private val licensesOnClickListener = Preference.OnPreferenceClickListener { - Timber.d("Log: licensesClick: Started") val intent = Intent(requireContext(), LicensesActivity::class.java) startActivity(intent) true } private val githubOnClickListener = Preference.OnPreferenceClickListener { - Timber.d("Log: githubClick: Started") val uriUrl = Uri.parse("https://github.com/MarcDonald/Earworm") val launchBrowser = Intent(Intent.ACTION_VIEW) launchBrowser.data = uriUrl @@ -95,15 +90,11 @@ class SettingsFragment : PreferenceFragmentCompat() { } private fun matchSummaryToSelection(preference: Preference, value: String) { - Timber.d("Log: themeOnChangeListener: Started") - Timber.d("Log: themeOnChangeListener: Value = $value") - if(preference is ListPreference) { val index = preference.findIndexOfValue(value) preference.setSummary( if(index >= 0) { - Timber.d("Log: BindPreferenceSummaryToValue: Setting summary to ${preference.entries[index]}") preference.entries[index] } else { Timber.w("Log: BindPreferenceSummaryToValue: Index < 0") @@ -111,7 +102,6 @@ class SettingsFragment : PreferenceFragmentCompat() { }) } else { - Timber.d("Log: BindPreferenceSummaryToValue: Setting summary to $value") preference.summary = value } } diff --git a/app/src/main/java/app/marcdev/earworm/uicomponents/AddItemDatePickerDialog.kt b/app/src/main/java/app/marcdev/earworm/uicomponents/AddItemDatePickerDialog.kt new file mode 100644 index 0000000..a8fd6e7 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/uicomponents/AddItemDatePickerDialog.kt @@ -0,0 +1,52 @@ +package app.marcdev.earworm.uicomponents + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.DatePicker +import app.marcdev.earworm.R +import app.marcdev.earworm.internal.base.EarwormDialogFragment +import com.google.android.material.button.MaterialButton +import java.util.* + +class AddItemDatePickerDialog(private val calendarArg: Calendar?, + private val okClick: (Int, Int, Int) -> Unit) + : EarwormDialogFragment() { + + // + private lateinit var datePicker: DatePicker + private lateinit var cancelButton: MaterialButton + private lateinit var okButton: MaterialButton + // + + private val day: Int + get() = datePicker.dayOfMonth + private val month: Int + get() = datePicker.month + private val year: Int + get() = datePicker.year + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.dialog_datepicker, container, false) + bindViews(view) + setDate() + return view + } + + private fun bindViews(view: View) { + datePicker = view.findViewById(R.id.datepicker) + cancelButton = view.findViewById(R.id.btn_datepicker_cancel) + cancelButton.setOnClickListener { dismiss() } + okButton = view.findViewById(R.id.btn_datepicker_ok) + okButton.setOnClickListener { + okClick(day, month, year) + } + } + + private fun setDate() { + calendarArg?.let { calendar -> + datePicker.updateDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/uicomponents/BinaryOptionDialog.kt b/app/src/main/java/app/marcdev/earworm/uicomponents/BinaryOptionDialog.kt new file mode 100644 index 0000000..049b655 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/uicomponents/BinaryOptionDialog.kt @@ -0,0 +1,193 @@ +package app.marcdev.earworm.uicomponents + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import app.marcdev.earworm.R +import app.marcdev.earworm.internal.base.EarwormDialogFragment +import com.google.android.material.button.MaterialButton + +class BinaryOptionDialog : EarwormDialogFragment() { + + // UI Components + private lateinit var negativeButton: MaterialButton + private lateinit var positiveButton: MaterialButton + private lateinit var titleDisplay: TextView + private lateinit var messageDisplay: TextView + + // To set + private var negativeButtonText = "" + private var positiveButtonText = "" + private var titleText = "" + private var messageText = "" + private var negativeButtonOnClick: () -> Unit = {} + private var dismissAfterNegativeClick = true + private var positiveButtonOnClick: () -> Unit = {} + private var dismissAfterPositiveClick = true + private var isTitleVisible = true + private var isMessageVisible = true + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.dialog_binary_option, container, false) + bindViews(view) + setContent() + return view + } + + private fun bindViews(view: View) { + negativeButton = view.findViewById(R.id.btn_binary_dialog_negative) + positiveButton = view.findViewById(R.id.btn_binary_dialog_positive) + + titleDisplay = view.findViewById(R.id.txt_binary_dialog_title) + messageDisplay = view.findViewById(R.id.txt_binary_dialog_message) + } + + private fun setContent() { + if(titleText.isNotBlank()) { + titleDisplay.text = titleText + } + + if(messageText.isNotBlank()) { + messageDisplay.text = messageText + } + + if(negativeButtonText.isNotBlank()) { + negativeButton.text = negativeButtonText + } + + if(positiveButtonText.isNotBlank()) { + positiveButton.text = positiveButtonText + } + + negativeButton.setOnClickListener { + negativeButtonOnClick() + if(dismissAfterNegativeClick) + dismiss() + } + + positiveButton.setOnClickListener { + positiveButtonOnClick() + if(dismissAfterPositiveClick) + dismiss() + } + + if(isTitleVisible) + titleDisplay.visibility = View.VISIBLE + else + titleDisplay.visibility = View.GONE + + if(isMessageVisible) + messageDisplay.visibility = View.VISIBLE + else + messageDisplay.visibility = View.GONE + } + + private fun setNegativeButton(text: String, onClick: () -> Unit, dismissAfter: Boolean) { + negativeButtonText = text + negativeButtonOnClick = onClick + dismissAfterNegativeClick = dismissAfter + } + + private fun setPositiveButton(text: String, onClick: () -> Unit, dismissAfter: Boolean) { + positiveButtonText = text + positiveButtonOnClick = onClick + dismissAfterPositiveClick = dismissAfter + } + + private fun setTitle(text: String) { + if(view != null) { + titleDisplay.text = text + } else { + titleText = text + } + } + + private fun setMessage(text: String) { + if(view != null) { + messageDisplay.text = text + } else { + messageText = text + } + } + + private fun setTitleVisibility(isVisible: Boolean) { + isTitleVisible = isVisible + } + + private fun setMessageVisibility(isVisible: Boolean) { + isMessageVisible = isVisible + } + + class Builder { + // To set + private var negativeButtonText = "" + private var positiveButtonText = "" + private var titleText = "" + private var messageText = "" + private var negativeButtonOnClick: () -> Unit = {} + private var dismissAfterNegativeClick = true + private var positiveButtonOnClick: () -> Unit = {} + private var dismissAfterPositiveClick = true + private var isTitleVisible = true + private var isMessageVisible = true + + fun setTitle(text: String): Builder { + titleText = text + return this + } + + fun setMessage(text: String): Builder { + messageText = text + return this + } + + fun setNegativeButton(text: String, onClick: () -> Unit): Builder { + negativeButtonText = text + negativeButtonOnClick = onClick + return this + } + + fun setPositiveButton(text: String, onClick: () -> Unit): Builder { + positiveButtonText = text + positiveButtonOnClick = onClick + return this + } + + fun setNegativeButton(text: String, onClick: () -> Unit, dismissAfter: Boolean): Builder { + negativeButtonText = text + negativeButtonOnClick = onClick + dismissAfterNegativeClick = dismissAfter + return this + } + + fun setPositiveButton(text: String, onClick: () -> Unit, dismissAfter: Boolean): Builder { + positiveButtonText = text + positiveButtonOnClick = onClick + dismissAfterPositiveClick = dismissAfter + return this + } + + fun setTitleVisible(isVisible: Boolean): Builder { + isTitleVisible = isVisible + return this + } + + fun setMessageVisible(isVisible: Boolean): Builder { + isMessageVisible = isVisible + return this + } + + fun build(): BinaryOptionDialog { + val dialog = BinaryOptionDialog() + dialog.setTitle(titleText) + dialog.setMessage(messageText) + dialog.setNegativeButton(negativeButtonText, negativeButtonOnClick, dismissAfterNegativeClick) + dialog.setPositiveButton(positiveButtonText, positiveButtonOnClick, dismissAfterPositiveClick) + dialog.setTitleVisibility(isTitleVisible) + dialog.setMessageVisibility(isMessageVisible) + return dialog + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDatePickerDialog.kt b/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDatePickerDialog.kt new file mode 100644 index 0000000..9867c79 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDatePickerDialog.kt @@ -0,0 +1,74 @@ +package app.marcdev.earworm.uicomponents + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.DatePicker +import app.marcdev.earworm.R +import app.marcdev.earworm.internal.DEFAULT_FILTER +import app.marcdev.earworm.internal.base.EarwormDialogFragment +import com.google.android.material.button.MaterialButton +import java.util.* + +class FilterDatePickerDialog(private val okClick: (Int, Int, Int) -> Unit, + private val isStart: Boolean, + private val calendar: Calendar?) + : EarwormDialogFragment() { + + // + private lateinit var datePicker: DatePicker + private lateinit var cancelButton: MaterialButton + private lateinit var okButton: MaterialButton + private lateinit var startEndButton: MaterialButton + // + + private val day: Int + get() = datePicker.dayOfMonth + private val month: Int + get() = datePicker.month + private val year: Int + get() = datePicker.year + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.dialog_datepicker_filter, container, false) + bindViews(view) + setDate() + return view + } + + private fun bindViews(view: View) { + datePicker = view.findViewById(R.id.datepicker_filter) + cancelButton = view.findViewById(R.id.btn_datepicker_filter_cancel) + cancelButton.setOnClickListener { dismiss() } + okButton = view.findViewById(R.id.btn_datepicker_filter_ok) + okButton.setOnClickListener { + okClick(day, month, year) + } + startEndButton = view.findViewById(R.id.btn_datepicker_filter_start_end) + startEndButton.setOnClickListener { + if(isStart) { + okClick(DEFAULT_FILTER.startDay, DEFAULT_FILTER.startMonth, DEFAULT_FILTER.startYear) + } else { + okClick(DEFAULT_FILTER.endDay, DEFAULT_FILTER.endMonth, DEFAULT_FILTER.endYear) + } + resetDate() + } + + if(isStart) + startEndButton.text = resources.getString(R.string.start) + else + startEndButton.text = resources.getString(R.string.end) + } + + private fun resetDate() { + val today = Calendar.getInstance() + datePicker.updateDate(today.get(Calendar.YEAR), today.get(Calendar.MONTH), today.get(Calendar.DAY_OF_MONTH)) + } + + private fun setDate() { + calendar?.let { calendar -> + datePicker.updateDate(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDialog.kt b/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDialog.kt index f1c2394..c17863f 100644 --- a/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDialog.kt +++ b/app/src/main/java/app/marcdev/earworm/uicomponents/FilterDialog.kt @@ -1,161 +1,127 @@ package app.marcdev.earworm.uicomponents -import android.app.Dialog -import android.content.Context -import android.graphics.Color -import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.widget.CheckBox import android.widget.CompoundButton -import android.widget.DatePicker import app.marcdev.earworm.R -import app.marcdev.earworm.mainscreen.MainFragmentPresenter -import app.marcdev.earworm.utils.DEFAULT_FILTER +import app.marcdev.earworm.internal.DEFAULT_FILTER +import app.marcdev.earworm.internal.base.EarwormDialogFragment import app.marcdev.earworm.utils.ItemFilter import app.marcdev.earworm.utils.formatDateForDisplay import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip -import timber.log.Timber import java.util.* -class FilterDialog(context: Context, private val presenter: MainFragmentPresenter) : Dialog(context) { +class FilterDialog(private val okClick: (ItemFilter) -> Unit) : EarwormDialogFragment() { private lateinit var displaySongCheckbox: CheckBox private lateinit var displayAlbumCheckbox: CheckBox private lateinit var displayArtistCheckbox: CheckBox private lateinit var startDateDisplay: Chip private lateinit var endDateDisplay: Chip - private lateinit var startDatePickerDialog: Dialog - private lateinit var endDatePickerDialog: Dialog - var activeFilter: ItemFilter = DEFAULT_FILTER.copy() - - init { - Timber.d("Log: FilterDialog Init: Started") - setContentView(R.layout.dialog_filter) - window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - bindViews() + private lateinit var startDatePickerDialog: FilterDatePickerDialog + private lateinit var endDatePickerDialog: FilterDatePickerDialog + private var activeFilter: ItemFilter = DEFAULT_FILTER.copy() + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val view = inflater.inflate(R.layout.dialog_filter, container, false) + bindViews(view) initCheckboxes() + return view } - private fun bindViews() { - Timber.d("Log: bindViews: Started") - - initStartDatePickerDialog() - initEndDatePickerDialog() - - this.startDateDisplay = findViewById(R.id.chip_filter_start) + private fun bindViews(view: View) { + startDateDisplay = view.findViewById(R.id.chip_filter_start) startDateDisplay.setOnClickListener { - Timber.d("Log: startDateClickListener: Started") - startDatePickerDialog.show() + startDatePickerDialog.show(requireFragmentManager(), "Start Date Picker") } - this.endDateDisplay = findViewById(R.id.chip_filter_end) + this.endDateDisplay = view.findViewById(R.id.chip_filter_end) endDateDisplay.setOnClickListener { - Timber.d("Log: endDateClickListener: Started") - endDatePickerDialog.show() + endDatePickerDialog.show(requireFragmentManager(), "End Date Picker") } - this.displaySongCheckbox = findViewById(R.id.chk_filter_song) + this.displaySongCheckbox = view.findViewById(R.id.chk_filter_song) displaySongCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> activeFilter.includeSongs = isChecked } - this.displayAlbumCheckbox = findViewById(R.id.chk_filter_album) + this.displayAlbumCheckbox = view.findViewById(R.id.chk_filter_album) displayAlbumCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> activeFilter.includeAlbums = isChecked } - this.displayArtistCheckbox = findViewById(R.id.chk_filter_artist) + this.displayArtistCheckbox = view.findViewById(R.id.chk_filter_artist) displayArtistCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> activeFilter.includeArtists = isChecked } - val submitButton: MaterialButton = findViewById(R.id.btn_filter_ok) + val submitButton: MaterialButton = view.findViewById(R.id.btn_filter_ok) submitButton.setOnClickListener { - Timber.d("Log: submitButtonOnClickListener: Started") - presenter.getAllItems(activeFilter) + okClick(activeFilter) dismiss() } + + initStartDatePickerDialog() + initEndDatePickerDialog() } private fun initStartDatePickerDialog() { - Timber.d("Log: initStartDatePickerDialog: Started") - - this.startDatePickerDialog = Dialog(context) - startDatePickerDialog.setContentView(R.layout.dialog_datepicker_filter) - startDatePickerDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - val datePicker: DatePicker = startDatePickerDialog.findViewById(R.id.datepicker_filter) - - val cancelButton: MaterialButton = startDatePickerDialog.findViewById(R.id.btn_datepicker_filter_cancel) - cancelButton.setOnClickListener { - Timber.d("Log: cancelButtonOnClickListener: Started") - startDatePickerDialog.dismiss() - } - - val okButton: MaterialButton = startDatePickerDialog.findViewById(R.id.btn_datepicker_filter_ok) - okButton.setOnClickListener { - Timber.d("Log: okButtonOnClickListener: Started") - activeFilter.startDay = datePicker.dayOfMonth - activeFilter.startMonth = datePicker.month - activeFilter.startYear = datePicker.year - startDateDisplay.text = formatDateForDisplay(datePicker.dayOfMonth, datePicker.month, datePicker.year) - startDatePickerDialog.dismiss() - } - - val startButton: MaterialButton = startDatePickerDialog.findViewById(R.id.btn_datepicker_filter_start_end) - startButton.setOnClickListener { - Timber.d("Log: startButtonOnClickListener: Started") - activeFilter.startDay = DEFAULT_FILTER.startDay - activeFilter.startMonth = DEFAULT_FILTER.startMonth - activeFilter.startYear = DEFAULT_FILTER.startYear - startDateDisplay.text = context.resources.getString(R.string.start) - val todayCalendar = Calendar.getInstance() - datePicker.updateDate(todayCalendar.get(Calendar.YEAR), todayCalendar.get(Calendar.MONTH), todayCalendar.get(Calendar.DAY_OF_MONTH)) - startDatePickerDialog.dismiss() + if(activeFilter.startDay == DEFAULT_FILTER.startDay && activeFilter.startMonth == DEFAULT_FILTER.startMonth && activeFilter.startYear == DEFAULT_FILTER.startYear) { + startDateDisplay.text = resources.getString(R.string.start) + this.startDatePickerDialog = FilterDatePickerDialog(::startDatePickerOkClick, true, null) + } else { + startDateDisplay.text = formatDateForDisplay(activeFilter.startDay, activeFilter.startMonth, activeFilter.startYear) + val calendar = Calendar.getInstance() + calendar.set(Calendar.DAY_OF_MONTH, activeFilter.startDay) + calendar.set(Calendar.MONTH, activeFilter.startMonth) + calendar.set(Calendar.YEAR, activeFilter.startYear) + this.startDatePickerDialog = FilterDatePickerDialog(::startDatePickerOkClick, true, calendar) } } private fun initEndDatePickerDialog() { - Timber.d("Log: initEndDatePickerDialog: Started") - - this.endDatePickerDialog = Dialog(context) - endDatePickerDialog.setContentView(R.layout.dialog_datepicker_filter) - endDatePickerDialog.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - val datePicker: DatePicker = endDatePickerDialog.findViewById(R.id.datepicker_filter) - - val cancelButton: MaterialButton = endDatePickerDialog.findViewById(R.id.btn_datepicker_filter_cancel) - cancelButton.setOnClickListener { - Timber.d("Log: cancelButtonOnClickListener: Started") - endDatePickerDialog.dismiss() + if(activeFilter.endDay == DEFAULT_FILTER.endDay && activeFilter.endMonth == DEFAULT_FILTER.endMonth && activeFilter.endYear == DEFAULT_FILTER.endYear) { + endDateDisplay.text = resources.getString(R.string.end) + this.endDatePickerDialog = FilterDatePickerDialog(::endDatePickerOkClick, false, null) + } else { + endDateDisplay.text = formatDateForDisplay(activeFilter.endDay, activeFilter.endMonth, activeFilter.endYear) + val calendar = Calendar.getInstance() + calendar.set(Calendar.DAY_OF_MONTH, activeFilter.endDay) + calendar.set(Calendar.MONTH, activeFilter.endMonth) + calendar.set(Calendar.YEAR, activeFilter.endYear) + this.endDatePickerDialog = FilterDatePickerDialog(::endDatePickerOkClick, false, calendar) } + } - val okButton: MaterialButton = endDatePickerDialog.findViewById(R.id.btn_datepicker_filter_ok) - okButton.setOnClickListener { - Timber.d("Log: okButtonOnClickListener: Started") - activeFilter.endDay = datePicker.dayOfMonth - activeFilter.endMonth = datePicker.month - activeFilter.endYear = datePicker.year - endDateDisplay.text = formatDateForDisplay(datePicker.dayOfMonth, datePicker.month, datePicker.year) - endDatePickerDialog.dismiss() + private fun startDatePickerOkClick(day: Int, month: Int, year: Int) { + activeFilter.startDay = day + activeFilter.startMonth = month + activeFilter.startYear = year + if(day == DEFAULT_FILTER.startDay && month == DEFAULT_FILTER.startMonth && year == DEFAULT_FILTER.startYear) { + startDateDisplay.text = resources.getString(R.string.start) + } else { + startDateDisplay.text = formatDateForDisplay(activeFilter.startDay, activeFilter.startMonth, activeFilter.startYear) } + startDatePickerDialog.dismiss() + } - val endButton: MaterialButton = endDatePickerDialog.findViewById(R.id.btn_datepicker_filter_start_end) - endButton.text = context.resources.getString(R.string.end) - endButton.setOnClickListener { - Timber.d("Log: startButtonOnClickListener: Started") - activeFilter.endDay = DEFAULT_FILTER.endDay - activeFilter.endMonth = DEFAULT_FILTER.endMonth - activeFilter.endYear = DEFAULT_FILTER.endYear - endDateDisplay.text = context.resources.getString(R.string.end) - val todayCalendar = Calendar.getInstance() - datePicker.updateDate(todayCalendar.get(Calendar.YEAR), todayCalendar.get(Calendar.MONTH), todayCalendar.get(Calendar.DAY_OF_MONTH)) - endDatePickerDialog.dismiss() + private fun endDatePickerOkClick(day: Int, month: Int, year: Int) { + activeFilter.endDay = day + activeFilter.endMonth = month + activeFilter.endYear = year + if(day == DEFAULT_FILTER.endDay && month == DEFAULT_FILTER.endMonth && year == DEFAULT_FILTER.endYear) { + endDateDisplay.text = resources.getString(R.string.end) + } else { + endDateDisplay.text = formatDateForDisplay(activeFilter.endDay, activeFilter.endMonth, activeFilter.endYear) } + endDatePickerDialog.dismiss() } private fun initCheckboxes() { - Timber.d("Log: initCheckboxes: Started") displaySongCheckbox.isChecked = activeFilter.includeSongs displayAlbumCheckbox.isChecked = activeFilter.includeAlbums displayArtistCheckbox.isChecked = activeFilter.includeArtists diff --git a/app/src/main/java/app/marcdev/earworm/utils/DateTimeUtils.kt b/app/src/main/java/app/marcdev/earworm/utils/DateTimeUtils.kt new file mode 100644 index 0000000..81da9ca --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/utils/DateTimeUtils.kt @@ -0,0 +1,43 @@ +package app.marcdev.earworm.utils + +import android.text.format.DateFormat +import java.util.* + +/** + * Converts date to a format suitable for display + * @param day The day + * @param month The month (indexed at 0 the same as the Java calendar, so January is 0) + * @param year The year + */ +fun formatDateForDisplay(day: Int, month: Int, year: Int): String { + val calendar = Calendar.getInstance() + calendar.set(Calendar.DAY_OF_MONTH, day) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.YEAR, year) + return formatDateForDisplay(calendar) +} + +/** + * Converts date to a format suitable for display + * @param calendar Date to convert + */ +fun formatDateForDisplay(calendar: Calendar): String { + val datePattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMyyyy") + val formattedDate = DateFormat.format(datePattern, calendar) + return formattedDate.toString() +} + +/** + * Formats the date for displaying on a header + * @param month Integer value of the month, indexed at 0 + * @param year Integer value of the year + * @return Full name of the month in string format + */ +fun formatDateForHeaderDisplay(month: Int, year: Int): String { + val calendar = Calendar.getInstance() + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.YEAR, year) + val datePattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "MMMMyyyy") + val formattedDate = DateFormat.format(datePattern, calendar) + return formattedDate.toString() +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/utils/EarwormUtils.kt b/app/src/main/java/app/marcdev/earworm/utils/EarwormUtils.kt index 9078c1f..f6f0f73 100644 --- a/app/src/main/java/app/marcdev/earworm/utils/EarwormUtils.kt +++ b/app/src/main/java/app/marcdev/earworm/utils/EarwormUtils.kt @@ -4,58 +4,13 @@ import android.content.Context import android.graphics.PorterDuff import android.graphics.drawable.Drawable import android.preference.PreferenceManager -import android.widget.ImageButton -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager +import android.widget.ImageView import app.marcdev.earworm.R -import timber.log.Timber - -// Song types -const val SONG = 0 -const val ALBUM = 1 -const val ARTIST = 2 -const val GENRE = 3 -const val HEADER = 4 - -val DEFAULT_FILTER = ItemFilter(1, 0, 1900, 31, 11, 2099, true, true, true, "") - -// Theme IDs -const val LIGHT_THEME = 0 -const val DARK_THEME = 1 - -// Preference keys -const val PREF_THEME = "pref_theme" -const val PREF_SHOW_TIPS = "pref_show_tips" -const val PREF_BUILD_NUMBER = "pref_build_number" -const val PREF_LICENSES = "pref_licenses" -const val PREF_GITHUB = "pref_github" -const val PREF_CLEAR_INPUTS = "pref_clear_inputs_on_type_change" - -/** - * Replaces a fragment in a frame with another fragment - * @param fragment The fragment to display - * @param fragmentManager The Fragment Manager - * @param frameId The ID of the frame to display the new fragment in - */ -fun setFragment(fragment: Fragment, fragmentManager: FragmentManager, frameId: Int) { - Timber.d("Log: setFragment: Replacing frame $frameId with fragment $fragment") - val fragmentTransaction = fragmentManager.beginTransaction() - fragmentTransaction.replace(frameId, fragment) - fragmentTransaction.commit() -} - -/** - * Converts date to a format suitable for display - * @param day The day - * @param month The month (indexed at 0 the same as the Java calendar, so January is 0) - * @param year The year - */ -fun formatDateForDisplay(day: Int, month: Int, year: Int): String { - Timber.d("Log: formatDateForDisplay: Started with day = $day, month = $month, year = $year") - // Add 1 to month to make it non-zero indexed (January will now be 1 rather than 0) - return "$day/${month + 1}/$year" -} +import app.marcdev.earworm.internal.DARK_THEME +import app.marcdev.earworm.internal.LIGHT_THEME +import app.marcdev.earworm.internal.PREF_THEME +// /** * Changes the color of a drawable in an ImageView to indicate whether it is activated or not. * Deactivated will change the color to either black or 70% white depending on the theme @@ -63,9 +18,7 @@ fun formatDateForDisplay(day: Int, month: Int, year: Int): String { * @param button The button to change the color of * @param isActivated Whether or not the button should be put into the activated state */ -fun changeColorOfImageButtonDrawable(context: Context, button: ImageButton, isActivated: Boolean) { - Timber.v("Log: changeColorOfImageButtonDrawable: Started") - +fun changeColorOfImageViewDrawable(context: Context, button: ImageView, isActivated: Boolean) { when { isActivated -> button.setColorFilter(context.getColor(R.color.colorAccent)) (getTheme(context) == DARK_THEME && !isActivated) -> button.setColorFilter(context.getColor(R.color.white60)) @@ -81,8 +34,6 @@ fun changeColorOfImageButtonDrawable(context: Context, button: ImageButton, isAc * @param isActivated Whether or not the button should be put into the activated state */ fun changeColorOfDrawable(context: Context, drawable: Drawable, isActivated: Boolean) { - Timber.v("Log: changeColorOfDrawable: Started") - when { isActivated -> drawable.setColorFilter(context.getColor(R.color.colorAccent), PorterDuff.Mode.SRC_IN) (getTheme(context) == DARK_THEME && !isActivated) -> drawable.setColorFilter((context.getColor(R.color.white60)), PorterDuff.Mode.SRC_IN) @@ -90,39 +41,17 @@ fun changeColorOfDrawable(context: Context, drawable: Drawable, isActivated: Boo } } -/** - * Gets the full name of a month based on it's 0 indexed number - * @param month Integer value of the month, indexed at 0 - * @param context Context - * @return Full name of the month in string format - */ -fun getMonthName(month: Int, context: Context): String { - val monthArray = context.resources.getStringArray(R.array.months) - return monthArray[month] -} - -/** - * Gets the path of the application's image storage - * @param context Context - */ -fun getArtworkDirectory(context: Context): String { - Timber.d("Log: getArtworkDirectory: Started") - val returnValue = context.filesDir.path + "/artwork/" - Timber.d("Log: getArtworkDirectory: Returning $returnValue") - return returnValue -} - /** * Checks the shared preferences to see if the user has selected dark mode * @param context Context */ fun getTheme(context: Context): Int { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - val theme = prefs.getString("pref_theme", context.resources.getString(R.string.light)) - return when(theme) { + return when(prefs.getString(PREF_THEME, context.resources.getString(R.string.light))) { context.resources.getString(R.string.light) -> LIGHT_THEME context.resources.getString(R.string.dark) -> DARK_THEME else -> -1 } -} \ No newline at end of file +} +// \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/utils/FileUtils.kt b/app/src/main/java/app/marcdev/earworm/utils/FileUtils.kt new file mode 100644 index 0000000..caed905 --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/utils/FileUtils.kt @@ -0,0 +1,11 @@ +package app.marcdev.earworm.utils + +import java.io.File + +interface FileUtils { + fun deleteImage(imageName: String) + + fun saveImage(file: File) + + val artworkDirectory: String +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/utils/FileUtilsImpl.kt b/app/src/main/java/app/marcdev/earworm/utils/FileUtilsImpl.kt new file mode 100644 index 0000000..7395a2e --- /dev/null +++ b/app/src/main/java/app/marcdev/earworm/utils/FileUtilsImpl.kt @@ -0,0 +1,35 @@ +package app.marcdev.earworm.utils + +import android.content.Context +import timber.log.Timber +import java.io.File + +class FileUtilsImpl(private val context: Context) : FileUtils { + + override fun deleteImage(imageName: String) { + val filePath = artworkDirectory + imageName + val file = File(filePath) + if(file.exists()) { + file.delete() + } else { + Timber.w("Log: deleteImage: File doesn't exist") + } + } + + override fun saveImage(file: File) { + val toPath = artworkDirectory + file.name + val toFile = File(toPath) + if(toFile.compareTo(file) != 0) { + try { + file.copyTo(toFile, true) + } catch(e: NoSuchFileException) { + Timber.e("Log: saveImage: $e") + } + } else { + Timber.d("Log: saveImage: No need to save as file already exists in storage") + } + } + + override val artworkDirectory: String + get() = context.filesDir.path + "/artwork/" +} \ No newline at end of file diff --git a/app/src/main/java/app/marcdev/earworm/utils/ListUtils.kt b/app/src/main/java/app/marcdev/earworm/utils/ListUtils.kt deleted file mode 100644 index 563e2d4..0000000 --- a/app/src/main/java/app/marcdev/earworm/utils/ListUtils.kt +++ /dev/null @@ -1,140 +0,0 @@ -package app.marcdev.earworm.utils - -import app.marcdev.earworm.database.FavouriteItem -import timber.log.Timber - -/** - * Filters a list based on a filter input by the user - * @param allItems The complete list of items from the database - * @param filter The filter to apply - * @return Filtered list - */ -fun applyFilter(allItems: MutableList, filter: ItemFilter): MutableList { - val filteredItems = mutableListOf() - filteredItems.addAll(allItems) - - val startDayTwoDigit = if(filter.startDay < 10) - "0${filter.startDay}" - else { - "${filter.startDay}" - } - - val startMonthTwoDigit = if(filter.startMonth < 10) - "0${filter.startMonth}" - else { - "${filter.startMonth}" - } - - val filterCompleteDateStart: Int = "${filter.startYear}$startMonthTwoDigit$startDayTwoDigit".toInt() - - val endDayTwoDigit = if(filter.endDay < 10) - "0${filter.endDay}" - else { - "${filter.endDay}" - } - - val endMonthTwoDigit = if(filter.endMonth < 10) - "0${filter.endMonth}" - else { - "${filter.endMonth}" - } - - val filterCompleteDateEnd: Int = "${filter.endYear}$endMonthTwoDigit$endDayTwoDigit".toInt() - - for(x in 0 until allItems.size) { - if(allItems[x].type == SONG && !filter.includeSongs) { - filteredItems.remove(allItems[x]) - } - - if(allItems[x].type == ALBUM && !filter.includeAlbums) { - filteredItems.remove(allItems[x]) - } - - if(allItems[x].type == ARTIST && !filter.includeArtists) { - filteredItems.remove(allItems[x]) - } - - val dayTwoDigit = if(allItems[x].day < 10) - "0${allItems[x].day}" - else { - "${allItems[x].day}" - } - - val monthTwoDigit = if(allItems[x].month < 10) - "0${allItems[x].month}" - else { - "${allItems[x].month}" - } - - val completeDate = "${allItems[x].year}$monthTwoDigit$dayTwoDigit" - val completeDateI: Int = completeDate.toInt() - - if(completeDateI < filterCompleteDateStart || completeDateI > filterCompleteDateEnd) { - filteredItems.remove(allItems[x]) - } - - if(!(allItems[x].albumName.contains(filter.searchTerm, true)) - && !(allItems[x].artistName.contains(filter.searchTerm, true)) - && !(allItems[x].songName.contains(filter.searchTerm, true)) - && !(allItems[x].genre.contains(filter.searchTerm, true)) - ) { - filteredItems.remove(allItems[x]) - } - } - - return sortByDateDescending(filteredItems) -} - -/** - * Sorts a list by date in descending order - * @param items List to sort - * @return Sorted list of FavouriteItems - */ -fun sortByDateDescending(items: MutableList): MutableList { - val filteredItems = items.sortedWith( - compareBy( - { -it.year }, - { -it.month }, - { -it.day }, - { -it.id!! })) - - return filteredItems.toMutableList() -} - -/** - * Adds header items to a list on a monthly basis - * @param allItems List to add headers to (sorted by date descending) - * @return List with headers every new month - */ -fun addListHeaders(allItems: MutableList): List { - val listWithHeaders = mutableListOf() - listWithHeaders.addAll(allItems) - - var lastMonth = 12 - var lastYear = 9999 - if(allItems.isNotEmpty()) { - lastMonth = allItems.first().month + 1 - lastYear = allItems.first().year - } - - val headersToAdd = mutableListOf>() - - for(x in 0 until allItems.size) { - if(((allItems[x].month < lastMonth) && (allItems[x].year == lastYear)) - || (allItems[x].month > lastMonth) && (allItems[x].year < lastYear) - || (allItems[x].year < lastYear) - ) { - Timber.v("Log: addListHeaders: x = $x") - val header = FavouriteItem("", "", "", "", 0, allItems[x].month, allItems[x].year, HEADER, "") - lastMonth = allItems[x].month - lastYear = allItems[x].year - headersToAdd.add(Pair(x, header)) - } - } - - for((add, x) in (0 until headersToAdd.size).withIndex()) { - listWithHeaders.add(headersToAdd[x].first + add, headersToAdd[x].second) - } - - return listWithHeaders -} diff --git a/app/src/main/res/drawable/rounded_dialog_background.xml b/app/src/main/res/drawable/rounded_dialog_background.xml index 1f1014e..4f69b3d 100644 --- a/app/src/main/res/drawable/rounded_dialog_background.xml +++ b/app/src/main/res/drawable/rounded_dialog_background.xml @@ -1,11 +1,7 @@ - + - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_datepicker.xml b/app/src/main/res/layout/dialog_datepicker.xml index b91904d..e908d2b 100644 --- a/app/src/main/res/layout/dialog_datepicker.xml +++ b/app/src/main/res/layout/dialog_datepicker.xml @@ -2,8 +2,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> + xmlns:app="http://schemas.android.com/apk/res-auto"> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_edit_or_delete.xml b/app/src/main/res/layout/dialog_edit_or_delete.xml deleted file mode 100644 index 47ec010..0000000 --- a/app/src/main/res/layout/dialog_edit_or_delete.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml index cd957db..12148d0 100644 --- a/app/src/main/res/layout/dialog_filter.xml +++ b/app/src/main/res/layout/dialog_filter.xml @@ -1,7 +1,6 @@ @@ -187,7 +186,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/marginStandard" - android:backgroundTint="@color/positive" + style="@style/PositiveButton" android:text="@string/filter" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/toolbar_filter.xml b/app/src/main/res/layout/toolbar_filter.xml index 8900531..b939f3e 100644 --- a/app/src/main/res/layout/toolbar_filter.xml +++ b/app/src/main/res/layout/toolbar_filter.xml @@ -16,17 +16,17 @@ android:focusable="true" android:focusableInTouchMode="true"> - @@ -40,26 +40,26 @@ android:layout_marginTop="@dimen/toolbarIconMargin" android:layout_marginEnd="@dimen/toolbarIconMarginSmall" android:layout_marginBottom="@dimen/toolbarIconMargin" - android:inputType="text" - android:hint="@string/search" android:autofillHints="@string/search" + android:hint="@string/search" + android:inputType="text" android:singleLine="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/img_search" app:layout_constraintStart_toEndOf="@id/img_filter" app:layout_constraintTop_toTopOf="parent" /> - diff --git a/app/src/main/res/layout/toolbar_main.xml b/app/src/main/res/layout/toolbar_main.xml index c776ea8..4cb736e 100644 --- a/app/src/main/res/layout/toolbar_main.xml +++ b/app/src/main/res/layout/toolbar_main.xml @@ -1,44 +1,44 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/toolbar_main" + android:layout_width="match_parent" + android:layout_height="@dimen/appBarHeight" + android:background="@color/colorPrimary" + app:contentInsetEnd="0dp" + app:contentInsetLeft="0dp" + app:contentInsetRight="0dp" + app:contentInsetStart="0dp" + app:layout_scrollFlags="scroll|snap|enterAlways"> + android:layout_width="match_parent" + android:layout_height="match_parent"> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - diff --git a/app/src/main/res/layout/toolbar_settings.xml b/app/src/main/res/layout/toolbar_settings.xml index eb00be5..3419cb7 100644 --- a/app/src/main/res/layout/toolbar_settings.xml +++ b/app/src/main/res/layout/toolbar_settings.xml @@ -28,15 +28,15 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" /> - + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f71d525..944e43e 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,9 +4,10 @@ 4dp 16dp 56dp - 48dp + 36dp 16dp 8dp + 6dp 16dp 8dp 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0b1a775..c9f255e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -25,6 +25,8 @@ Shortcut Disabled Yes No + Title + Content Song name diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ab964e1..2894833 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -17,6 +17,8 @@ @color/lightThemeColorPrimary @style/Earworm.LightToolbar @color/lightThemeColorPrimaryDark + @color/positive + @color/negative @@ -39,6 +41,8 @@ @color/darkThemeColorPrimaryDark @color/darkThemeColorPrimary ?attr/navigationBarColor + @color/positive + @color/negative @@ -74,8 +78,20 @@ ?attr/transparentButtonBackground + + + + - -