diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 5b97b65da..1e6cd5115 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -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> = + 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? + 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> 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" + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt index 9f3d546e5..fa081ce7f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardAdapter.kt @@ -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(DashboardAdapterDiffCallback()) { +class DashboardAdapter @Inject constructor() : RecyclerView.Adapter() { - 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() + + fun submitList(newItems: List) { + 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() { + private class DiffCallback( + private val newList: List, + private val oldList: List + ) : 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 { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt index 283f57451..3392280bc 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardFragment.kt @@ -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(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(R.layout.fragme adapter = dashboardAdapter (itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false } + + itemTouchHelper.attachToRecyclerView(dashboardRecycler) } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt new file mode 100644 index 000000000..cf4097a4a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardItemMoveCallback.kt @@ -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) -> 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()) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt index 12374859d..0e24f0a14 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/dashboard/DashboardPresenter.kt @@ -68,6 +68,16 @@ class DashboardPresenter @Inject constructor( .launch("dashboard_pref") } + fun onDragAndDropEnd(list: List) { + 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) { 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 } } diff --git a/app/src/main/res/layout/item_dashboard_horizontal_group.xml b/app/src/main/res/layout/item_dashboard_horizontal_group.xml index a8532e6fb..bbd8f3517 100644 --- a/app/src/main/res/layout/item_dashboard_horizontal_group.xml +++ b/app/src/main/res/layout/item_dashboard_horizontal_group.xml @@ -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"> + app:layout_constraintTop_toTopOf="parent"> + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6323668a..c1b3a3ee4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -501,7 +501,7 @@ until %1$s No upcoming lessons - An error occurred while loading the lesson + An error occurred while loading the lessons Homework No homework to do