Skip to content

Commit

Permalink
Merge pull request #3 from merklol/develop
Browse files Browse the repository at this point in the history
v1.0.2
  • Loading branch information
merklol committed Dec 28, 2021
2 parents 3b15898 + f4cc28e commit 910b523
Show file tree
Hide file tree
Showing 72 changed files with 3,776 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2021 Maxim Smolyakov
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.maximapps.main.domain

import com.maximapps.core.domain.CrudRepository
import com.maximapps.core.domain.models.WorkoutNote
import com.maximapps.core.ext.format
import java.util.Date
import javax.inject.Inject

/**
* Adds a new workout note to the database.
*/
class AddNewWorkoutNoteUseCase @Inject constructor(
private val repository: CrudRepository
) {
suspend operator fun invoke() =
repository.addWorkoutNote(WorkoutNote(createdAt = Date().format()))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2021 Maxim Smolyakov
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.maximapps.main.domain

import com.maximapps.core.domain.CrudRepository
import com.maximapps.core.domain.models.WorkoutNote
import javax.inject.Inject

/**
* Deletes a workout note from the database.
*/
class DeleteWorkoutNoteUseCase @Inject constructor(
private val repository: CrudRepository
) {
suspend operator fun invoke(workoutNote: WorkoutNote) =
repository.deleteWorkoutNote(workoutNote)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2021 Maxim Smolyakov
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.maximapps.main.domain

import com.maximapps.core.domain.CrudRepository
import com.maximapps.core.domain.models.FetchResult
import com.maximapps.core.ui.models.ListState
import kotlinx.coroutines.flow.map
import javax.inject.Inject

/**
* Fetches workout notes from the database.
*/
class FetchWorkoutNotesUseCase @Inject constructor(
private val repository: CrudRepository
) {
operator fun invoke() = repository.fetchWorkoutNotes().map {
when (it) {
is FetchResult.Loading -> ListState()
is FetchResult.Empty -> ListState(isLoading = false, isEmpty = true)
is FetchResult.Result ->
ListState(isLoading = false, isEmpty = false, items = it.value)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2021 Maxim Smolyakov
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.maximapps.main.domain

import com.maximapps.core.domain.CrudRepository
import com.maximapps.core.domain.models.WorkoutNote
import javax.inject.Inject

/**
* Restores the last deleted workout note from the database.
*/
class RestoreWorkoutNoteUseCase @Inject constructor(private val repository: CrudRepository) {
suspend operator fun invoke(workoutNote: WorkoutNote) =
repository.restoreWorkoutNote(workoutNote)
}
31 changes: 31 additions & 0 deletions features/main/src/main/java/com/maximapps/main/ext/Fragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2021 Maxim Smolyakov
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.maximapps.main.ext

import android.content.Intent
import android.net.Uri
import androidx.fragment.app.Fragment

/**
* Opens a link in a browser.
*/
internal fun Fragment.openLink(uri: String) =
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(uri)))
65 changes: 65 additions & 0 deletions features/main/src/main/java/com/maximapps/main/ui/MainFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright (c) 2021 Maxim Smolyakov
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.maximapps.main.ui

import android.os.Bundle
import android.view.View
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.tabs.TabLayoutMediator
import com.maximapps.core.utils.navigation.GlobalDirections
import com.maximapps.core.utils.navigation.GlobalNavHost
import com.maximapps.main.R
import com.maximapps.main.databinding.FragmentMainBinding
import com.maximapps.main.ui.history.HistoryFragment
import com.maximapps.main.ui.pager.ViewPagerAdapter
import com.maximapps.main.ui.settings.SettingsFragment
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainFragment @Inject constructor(
private val directions: GlobalDirections,
private val host: GlobalNavHost,
private val versionName: String
) : Fragment(R.layout.fragment_main) {
private val binding: FragmentMainBinding by viewBinding()
private val tabIcons = arrayOf(
R.drawable.ic_baseline_bar_chart_24, R.drawable.ic_baseline_settings_24
)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setViewPagerAdapter(listOf(HistoryFragment(directions, host), SettingsFragment(versionName)))
setupTabLayoutWithViewPager()
}

private fun setViewPagerAdapter(fragments: List<Fragment>) {
binding.viewPager.adapter = ViewPagerAdapter(childFragmentManager, lifecycle, fragments)
}

private fun setupTabLayoutWithViewPager() {
TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
tab.icon = AppCompatResources.getDrawable(requireContext(), tabIcons[position])
}.attach()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2021 Maxim Smolyakov
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package com.maximapps.main.ui.history

import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import by.kirich1409.viewbindingdelegate.viewBinding
import com.maximapps.core.domain.models.WorkoutNote
import com.maximapps.core.ext.capitalized
import com.maximapps.core.ext.launchAfterStarted
import com.maximapps.core.ext.onStates
import com.maximapps.core.ui.recyclerview.ITEM_IS_AT_BOTTOM
import com.maximapps.core.ui.recyclerview.ITEM_IS_ON_TOP
import com.maximapps.core.utils.navigation.GlobalDirections
import com.maximapps.core.utils.navigation.GlobalNavHost
import com.maximapps.core.utils.navigation.models.toProperties
import com.maximapps.core.utils.navigation.navigate
import com.maximapps.core.ui.recyclerview.decorations.SpacingItemDecoration
import com.maximapps.core.ui.recyclerview.findItemPosition
import com.maximapps.core.ui.recyclerview.listAdapterOf
import com.maximapps.core.ui.recyclerview.setOnSwipedListener
import com.maximapps.core.utils.showUndoMessage
import com.maximapps.main.R
import com.maximapps.main.databinding.FragmentHistoryBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch

@AndroidEntryPoint
class HistoryFragment(
private val directions: GlobalDirections,
private val host: GlobalNavHost
) : Fragment(R.layout.fragment_history) {
private val viewModel: HistoryViewModel by viewModels()
private val binding: FragmentHistoryBinding by viewBinding()

private val adapter = listAdapterOf { ListViewHolder(it, ::onListItemClick) }

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupHistoryList(requireContext())
renderHistoryListStates()
binding.newWorkoutNote.setOnClickListener(::onClick)
}

private fun setupHistoryList(context: Context) {
binding.historyList.addItemDecoration(SpacingItemDecoration(context))
adapter.subscribeToDataChanges(::onDataChanged)
binding.historyList.adapter = adapter
onStates(viewModel, adapter::setState)
binding.historyList.setOnSwipedListener(context, ::onSwiped)
}

private fun renderHistoryListStates() = launchAfterStarted {
adapter.state.collectLatest {
binding.progressIndicator.isVisible = it.isLoading
binding.historyListHint.isVisible = it.isEmpty
binding.historyList.isInvisible = it.isLoading || it.isEmpty
binding.newWorkoutNote.isVisible = it.isEmpty || !it.isLoading
}
}

@Suppress("UNUSED_PARAMETER")
private fun onClick(view: View) = lifecycleScope.launch {
adapter.unsubscribeFromDataChanges()
val res = viewModel.addNewWorkoutNote()
navigate(directions.fromMainToWorkoutNote(res.toProperties()), host)
}

private fun onSwiped(position: Int) = with(adapter.getItemAt(position)) {
viewModel.deleteWorkoutNote(this)
showUndoMessage(requireView(), getString(R.string.undo_message, title.capitalized)) {
viewModel.restoreWorkoutNote(this)
}
}

private fun onListItemClick(workoutNote: WorkoutNote) {
adapter.unsubscribeFromDataChanges()
navigate(directions.fromMainToWorkoutNote(workoutNote.toProperties()), host)
}

private fun onDataChanged(itemPosition: Int) = with(binding.historyList) {
when (findItemPosition(itemPosition)) {
ITEM_IS_ON_TOP, ITEM_IS_AT_BOTTOM -> scrollToPosition(itemPosition)
}
}
}
Loading

0 comments on commit 910b523

Please sign in to comment.