forked from github/wulkanowy-mirror
Add drag and drop to dashboard tiles (#1415)
This commit is contained in:
parent
72ef5f428e
commit
626169de11
@ -2,9 +2,12 @@ package io.github.wulkanowy.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.squareup.moshi.Moshi
|
||||
import androidx.core.content.edit
|
||||
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
|
||||
import com.fredporciuncula.flow.preferences.Preference
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
@ -21,8 +24,13 @@ import javax.inject.Singleton
|
||||
class PreferencesRepository @Inject constructor(
|
||||
private val sharedPref: SharedPreferences,
|
||||
private val flowSharedPref: FlowSharedPreferences,
|
||||
@ApplicationContext val context: Context
|
||||
@ApplicationContext val context: Context,
|
||||
moshi: Moshi
|
||||
) {
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private val dashboardItemsPositionAdapter: JsonAdapter<Map<DashboardItem.Type, Int>> =
|
||||
moshi.adapter()
|
||||
|
||||
val startMenuIndex: Int
|
||||
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
|
||||
|
||||
@ -160,6 +168,19 @@ class PreferencesRepository @Inject constructor(
|
||||
R.bool.pref_default_optional_arithmetic_average
|
||||
)
|
||||
|
||||
var dashboardItemsPosition: Map<DashboardItem.Type, Int>?
|
||||
get() {
|
||||
val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null
|
||||
|
||||
return dashboardItemsPositionAdapter.fromJson(json)
|
||||
}
|
||||
set(value) = sharedPref.edit {
|
||||
putString(
|
||||
PREF_KEY_DASHBOARD_ITEMS_POSITION,
|
||||
dashboardItemsPositionAdapter.toJson(value)
|
||||
)
|
||||
}
|
||||
|
||||
val selectedDashboardTilesFlow: Flow<Set<DashboardItem.Tile>>
|
||||
get() = selectedDashboardTilesPreference.asFlow()
|
||||
.map { set ->
|
||||
@ -199,4 +220,9 @@ class PreferencesRepository @Inject constructor(
|
||||
|
||||
private fun getBoolean(id: String, default: Int) =
|
||||
sharedPref.getBoolean(id, context.resources.getBoolean(default))
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@ import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updateMarginsRelative
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
@ -38,10 +37,9 @@ import java.util.Timer
|
||||
import javax.inject.Inject
|
||||
import kotlin.concurrent.timer
|
||||
|
||||
class DashboardAdapter @Inject constructor() :
|
||||
ListAdapter<DashboardItem, RecyclerView.ViewHolder>(DashboardAdapterDiffCallback()) {
|
||||
class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
var lessonsTimer: Timer? = null
|
||||
private var lessonsTimer: Timer? = null
|
||||
|
||||
var onAccountTileClickListener: () -> Unit = {}
|
||||
|
||||
@ -63,7 +61,23 @@ class DashboardAdapter @Inject constructor() :
|
||||
|
||||
var onConferencesTileClickListener: () -> Unit = {}
|
||||
|
||||
override fun getItemViewType(position: Int) = getItem(position).type.ordinal
|
||||
val items = mutableListOf<DashboardItem>()
|
||||
|
||||
fun submitList(newItems: List<DashboardItem>) {
|
||||
val diffResult =
|
||||
DiffUtil.calculateDiff(DiffCallback(newItems, items.toMutableList()))
|
||||
|
||||
with(items) {
|
||||
clear()
|
||||
addAll(newItems)
|
||||
}
|
||||
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
override fun getItemCount() = items.size
|
||||
|
||||
override fun getItemViewType(position: Int) = items[position].type.ordinal
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
@ -119,7 +133,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun bindAccountViewHolder(accountViewHolder: AccountViewHolder, position: Int) {
|
||||
val item = getItem(position) as DashboardItem.Account
|
||||
val item = items[position] as DashboardItem.Account
|
||||
val student = item.student
|
||||
val isLoading = item.isLoading
|
||||
|
||||
@ -147,7 +161,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
horizontalGroupViewHolder: HorizontalGroupViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
val item = getItem(position) as DashboardItem.HorizontalGroup
|
||||
val item = items[position] as DashboardItem.HorizontalGroup
|
||||
val unreadMessagesCount = item.unreadMessagesCount
|
||||
val attendancePercentage = item.attendancePercentage
|
||||
val luckyNumber = item.luckyNumber
|
||||
@ -221,7 +235,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun bindGradesViewHolder(gradesViewHolder: GradesViewHolder, position: Int) {
|
||||
val item = getItem(position) as DashboardItem.Grades
|
||||
val item = items[position] as DashboardItem.Grades
|
||||
val subjectWithGrades = item.subjectWithGrades.orEmpty()
|
||||
val gradeTheme = item.gradeTheme
|
||||
val error = item.error
|
||||
@ -250,7 +264,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun bindLessonsViewHolder(lessonsViewHolder: LessonsViewHolder, position: Int) {
|
||||
val item = getItem(position) as DashboardItem.Lessons
|
||||
val item = items[position] as DashboardItem.Lessons
|
||||
val timetableFull = item.lessons
|
||||
val binding = lessonsViewHolder.binding
|
||||
|
||||
@ -519,7 +533,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun bindHomeworkViewHolder(homeworkViewHolder: HomeworkViewHolder, position: Int) {
|
||||
val item = getItem(position) as DashboardItem.Homework
|
||||
val item = items[position] as DashboardItem.Homework
|
||||
val homeworkList = item.homework.orEmpty()
|
||||
val error = item.error
|
||||
val isLoading = item.isLoading
|
||||
@ -557,7 +571,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
announcementsViewHolder: AnnouncementsViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
val item = getItem(position) as DashboardItem.Announcements
|
||||
val item = items[position] as DashboardItem.Announcements
|
||||
val schoolAnnouncementList = item.announcement.orEmpty()
|
||||
val error = item.error
|
||||
val isLoading = item.isLoading
|
||||
@ -594,7 +608,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun bindExamsViewHolder(examsViewHolder: ExamsViewHolder, position: Int) {
|
||||
val item = getItem(position) as DashboardItem.Exams
|
||||
val item = items[position] as DashboardItem.Exams
|
||||
val exams = item.exams.orEmpty()
|
||||
val error = item.error
|
||||
val isLoading = item.isLoading
|
||||
@ -630,7 +644,7 @@ class DashboardAdapter @Inject constructor() :
|
||||
conferencesViewHolder: ConferencesViewHolder,
|
||||
position: Int
|
||||
) {
|
||||
val item = getItem(position) as DashboardItem.Conferences
|
||||
val item = items[position] as DashboardItem.Conferences
|
||||
val conferences = item.conferences.orEmpty()
|
||||
val error = item.error
|
||||
val isLoading = item.isLoading
|
||||
@ -703,13 +717,20 @@ class DashboardAdapter @Inject constructor() :
|
||||
val adapter by lazy { DashboardConferencesAdapter() }
|
||||
}
|
||||
|
||||
class DashboardAdapterDiffCallback : DiffUtil.ItemCallback<DashboardItem>() {
|
||||
private class DiffCallback(
|
||||
private val newList: List<DashboardItem>,
|
||||
private val oldList: List<DashboardItem>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: DashboardItem, newItem: DashboardItem) =
|
||||
oldItem.type == newItem.type
|
||||
override fun getNewListSize() = newList.size
|
||||
|
||||
override fun areContentsTheSame(oldItem: DashboardItem, newItem: DashboardItem) =
|
||||
oldItem == newItem
|
||||
override fun getOldListSize() = oldList.size
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
newList[newItemPosition] == oldList[oldItemPosition]
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||
newList[newItemPosition].type == oldList[oldItemPosition].type
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
@ -8,6 +8,7 @@ import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
@ -68,6 +69,12 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
|
||||
|
||||
override fun initView() {
|
||||
val mainActivity = requireActivity() as MainActivity
|
||||
val itemTouchHelper = ItemTouchHelper(
|
||||
DashboardItemMoveCallback(
|
||||
dashboardAdapter,
|
||||
presenter::onDragAndDropEnd
|
||||
)
|
||||
)
|
||||
|
||||
dashboardAdapter.apply {
|
||||
onAccountTileClickListener = { mainActivity.pushView(AccountFragment.newInstance()) }
|
||||
@ -104,6 +111,8 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
|
||||
adapter = dashboardAdapter
|
||||
(itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
|
||||
}
|
||||
|
||||
itemTouchHelper.attachToRecyclerView(dashboardRecycler)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,55 @@
|
||||
package io.github.wulkanowy.ui.modules.dashboard
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.util.Collections
|
||||
|
||||
class DashboardItemMoveCallback(
|
||||
private val dashboardAdapter: DashboardAdapter,
|
||||
private var onUserInteractionEndListener: (List<DashboardItem>) -> Unit = {}
|
||||
) : ItemTouchHelper.Callback() {
|
||||
|
||||
override fun isLongPressDragEnabled() = true
|
||||
|
||||
override fun isItemViewSwipeEnabled() = false
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
//Not implemented
|
||||
}
|
||||
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
val dragFlags = if (viewHolder.bindingAdapterPosition != 0) {
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||
} else 0
|
||||
|
||||
return makeMovementFlags(dragFlags, 0)
|
||||
}
|
||||
|
||||
override fun canDropOver(
|
||||
recyclerView: RecyclerView,
|
||||
current: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
) = target.bindingAdapterPosition != 0
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val list = dashboardAdapter.items.toMutableList()
|
||||
|
||||
Collections.swap(list, viewHolder.bindingAdapterPosition, target.bindingAdapterPosition)
|
||||
|
||||
dashboardAdapter.submitList(list)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
|
||||
onUserInteractionEndListener(dashboardAdapter.items.toList())
|
||||
}
|
||||
}
|
@ -68,6 +68,16 @@ class DashboardPresenter @Inject constructor(
|
||||
.launch("dashboard_pref")
|
||||
}
|
||||
|
||||
fun onDragAndDropEnd(list: List<DashboardItem>) {
|
||||
dashboardItemLoadedList.clear()
|
||||
dashboardItemLoadedList.addAll(list)
|
||||
|
||||
val positionList =
|
||||
list.mapIndexed { index, dashboardItem -> Pair(dashboardItem.type, index) }.toMap()
|
||||
|
||||
preferencesRepository.dashboardItemsPosition = positionList
|
||||
}
|
||||
|
||||
fun loadData(forceRefresh: Boolean = false, tilesToLoad: Set<DashboardItem.Tile>) {
|
||||
val oldDashboardDataToLoad = dashboardTilesToLoad
|
||||
|
||||
@ -622,6 +632,7 @@ class DashboardPresenter @Inject constructor(
|
||||
|
||||
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
|
||||
val isForceRefreshError = forceRefresh && dashboardItem.error != null
|
||||
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
|
||||
|
||||
with(dashboardItemLoadedList) {
|
||||
removeAll { it.type == dashboardItem.type && !isForceRefreshError }
|
||||
@ -636,7 +647,12 @@ class DashboardPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
dashboardItemLoadedList.sortBy { tile -> dashboardItemsToLoad.single { it == tile.type }.ordinal }
|
||||
dashboardItemLoadedList.sortBy { tile ->
|
||||
dashboardItemsPosition?.getOrDefault(
|
||||
tile.type,
|
||||
tile.type.ordinal + 100
|
||||
) ?: tile.type.ordinal
|
||||
}
|
||||
|
||||
val isItemsLoaded =
|
||||
dashboardItemsToLoad.all { type -> dashboardItemLoadedList.any { it.type == type } }
|
||||
|
@ -4,8 +4,9 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="12dp"
|
||||
android:layout_marginVertical="2dp">
|
||||
android:paddingHorizontal="12dp"
|
||||
android:layout_marginVertical="2dp"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/dashboard_horizontal_group_item_lucky_container"
|
||||
|
@ -10,10 +10,12 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:includeFontPadding="false"
|
||||
android:maxLines="1"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/dashboard_grades_subitem_grade_container"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/dashboard_grades_subitem_grade_container"
|
||||
tools:text="Urządzenia techniki kompu..." />
|
||||
|
||||
<LinearLayout
|
||||
@ -25,6 +27,11 @@
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/dashboard_grades_subitem_title"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:ignore="UselessLeaf" />
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<include
|
||||
layout="@layout/subitem_dashboard_small_grade"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -501,7 +501,7 @@
|
||||
</plurals>
|
||||
<string name="dashboard_timetable_third_time">until %1$s</string>
|
||||
<string name="dashboard_timetable_no_lessons">No upcoming lessons</string>
|
||||
<string name="dashboard_timetable_error">An error occurred while loading the lesson</string>
|
||||
<string name="dashboard_timetable_error">An error occurred while loading the lessons</string>
|
||||
|
||||
<string name="dashboard_homework_title">Homework</string>
|
||||
<string name="dashboard_homework_no_homework">No homework to do</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user