diff --git a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt index c3dd4d0f..10a6c12a 100644 --- a/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/clock/activities/MainActivity.kt @@ -13,7 +13,10 @@ import org.fossify.clock.BuildConfig import org.fossify.clock.R import org.fossify.clock.adapters.ViewPagerAdapter import org.fossify.clock.databinding.ActivityMainBinding -import org.fossify.clock.extensions.* +import org.fossify.clock.extensions.config +import org.fossify.clock.extensions.getEnabledAlarms +import org.fossify.clock.extensions.rescheduleEnabledAlarms +import org.fossify.clock.extensions.updateWidgets import org.fossify.clock.helpers.* import org.fossify.commons.databinding.BottomTablayoutItemBinding import org.fossify.commons.extensions.* @@ -127,7 +130,11 @@ class MainActivity : SimpleActivity() { private fun setupOptionsMenu() { binding.mainToolbar.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { - R.id.sort -> getViewPagerAdapter()?.showAlarmSortDialog() + R.id.sort -> when (binding.viewPager.currentItem) { + TAB_ALARM_INDEX -> getViewPagerAdapter()?.showAlarmSortDialog() + TAB_TIMER_INDEX -> getViewPagerAdapter()?.showTimerSortDialog() + } + R.id.more_apps_from_us -> launchMoreAppsFromUsIntent() R.id.settings -> launchSettings() R.id.about -> launchAbout() @@ -139,7 +146,7 @@ class MainActivity : SimpleActivity() { private fun refreshMenuItems() { binding.mainToolbar.menu.apply { - findItem(R.id.sort).isVisible = binding.viewPager.currentItem == getTabIndex(TAB_ALARM) + findItem(R.id.sort).isVisible = binding.viewPager.currentItem == getTabIndex(TAB_ALARM) || binding.viewPager.currentItem == getTabIndex(TAB_TIMER) findItem(R.id.more_apps_from_us).isVisible = !resources.getBoolean(org.fossify.commons.R.bool.hide_google_relations) } } diff --git a/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt b/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt index 49d85ae7..5b0cb5f9 100644 --- a/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt +++ b/app/src/main/kotlin/org/fossify/clock/adapters/AlarmsAdapter.kt @@ -1,8 +1,12 @@ package org.fossify.clock.adapters +import android.annotation.SuppressLint import android.view.Menu +import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import org.fossify.clock.R import org.fossify.clock.activities.SimpleActivity import org.fossify.clock.databinding.ItemAlarmBinding @@ -14,17 +18,32 @@ import org.fossify.clock.interfaces.ToggleAlarmInterface import org.fossify.clock.models.Alarm import org.fossify.commons.adapters.MyRecyclerViewAdapter import org.fossify.commons.dialogs.ConfirmationDialog +import org.fossify.commons.extensions.applyColorFilter import org.fossify.commons.extensions.beVisibleIf import org.fossify.commons.extensions.toast +import org.fossify.commons.helpers.SORT_BY_CUSTOM +import org.fossify.commons.interfaces.ItemMoveCallback +import org.fossify.commons.interfaces.ItemTouchHelperContract +import org.fossify.commons.interfaces.StartReorderDragListener import org.fossify.commons.views.MyRecyclerView class AlarmsAdapter( activity: SimpleActivity, var alarms: ArrayList, val toggleAlarmInterface: ToggleAlarmInterface, recyclerView: MyRecyclerView, itemClick: (Any) -> Unit, -) : MyRecyclerViewAdapter(activity, recyclerView, itemClick) { +) : MyRecyclerViewAdapter(activity, recyclerView, itemClick), ItemTouchHelperContract { + + private var startReorderDragListener: StartReorderDragListener init { setupDragListener(true) + val touchHelper = ItemTouchHelper(ItemMoveCallback(this)) + touchHelper.attachToRecyclerView(recyclerView) + + startReorderDragListener = object : StartReorderDragListener { + override fun requestDrag(viewHolder: RecyclerView.ViewHolder) { + touchHelper.startDrag(viewHolder) + } + } } override fun getActionMenuId() = R.menu.cab_alarms @@ -49,9 +68,19 @@ class AlarmsAdapter( override fun getItemKeyPosition(key: Int) = alarms.indexOfFirst { it.id == key } - override fun onActionModeCreated() {} + @SuppressLint("NotifyDataSetChanged") + override fun onActionModeCreated() { + notifyDataSetChanged() + } - override fun onActionModeDestroyed() {} + @SuppressLint("NotifyDataSetChanged") + override fun onActionModeDestroyed() { + notifyDataSetChanged() + } + + override fun onRowClear(myViewHolder: ViewHolder?) {} + + override fun onRowSelected(myViewHolder: ViewHolder?) {} override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return createViewHolder(ItemAlarmBinding.inflate(layoutInflater, parent, false).root) @@ -59,14 +88,15 @@ class AlarmsAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { val alarm = alarms[position] - holder.bindView(alarm, true, true) { itemView, layoutPosition -> - setupView(itemView, alarm) + holder.bindView(alarm, true, true) { itemView, _ -> + setupView(itemView, alarm, holder) } bindViewHolder(holder) } override fun getItemCount() = alarms.size + @SuppressLint("NotifyDataSetChanged") fun updateItems(newItems: ArrayList) { alarms = newItems notifyDataSetChanged() @@ -87,10 +117,19 @@ class AlarmsAdapter( private fun getSelectedItems() = alarms.filter { selectedKeys.contains(it.id) } as ArrayList - private fun setupView(view: View, alarm: Alarm) { + @SuppressLint("ClickableViewAccessibility") + private fun setupView(view: View, alarm: Alarm, holder: ViewHolder) { val isSelected = selectedKeys.contains(alarm.id) ItemAlarmBinding.bind(view).apply { alarmHolder.isSelected = isSelected + alarmDragHandle.beVisibleIf(selectedKeys.isNotEmpty()) + alarmDragHandle.applyColorFilter(textColor) + alarmDragHandle.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + startReorderDragListener.requestDrag(holder) + } + false + } alarmTime.text = activity.getFormattedTime(alarm.timeInMinutes * 60, false, true) alarmTime.setTextColor(textColor) @@ -137,4 +176,19 @@ class AlarmsAdapter( } } } + + override fun onRowMoved(fromPosition: Int, toPosition: Int) { + alarms.swap(fromPosition, toPosition) + notifyItemMoved(fromPosition, toPosition) + saveAlarmsCustomOrder(alarms) + if (activity.config.alarmSort != SORT_BY_CUSTOM) { + activity.config.alarmSort = SORT_BY_CUSTOM + } + } + + private fun saveAlarmsCustomOrder(alarms: ArrayList) { + val alarmsCustomSortingIds = alarms.map { it.id } + + activity.config.alarmsCustomSorting = alarmsCustomSortingIds.joinToString { it.toString() } + } } diff --git a/app/src/main/kotlin/org/fossify/clock/adapters/StopwatchAdapter.kt b/app/src/main/kotlin/org/fossify/clock/adapters/StopwatchAdapter.kt index 7624ab94..1b4e8d26 100644 --- a/app/src/main/kotlin/org/fossify/clock/adapters/StopwatchAdapter.kt +++ b/app/src/main/kotlin/org/fossify/clock/adapters/StopwatchAdapter.kt @@ -1,5 +1,6 @@ package org.fossify.clock.adapters +import android.annotation.SuppressLint import android.view.Menu import android.view.View import android.view.ViewGroup @@ -52,6 +53,7 @@ class StopwatchAdapter(activity: SimpleActivity, var laps: ArrayList, recyc override fun getItemCount() = laps.size + @SuppressLint("NotifyDataSetChanged") fun updateItems(newItems: ArrayList) { lastLapId = 0 laps = newItems.clone() as ArrayList diff --git a/app/src/main/kotlin/org/fossify/clock/adapters/TimeZonesAdapter.kt b/app/src/main/kotlin/org/fossify/clock/adapters/TimeZonesAdapter.kt index 94f2abfd..ecdf6e50 100644 --- a/app/src/main/kotlin/org/fossify/clock/adapters/TimeZonesAdapter.kt +++ b/app/src/main/kotlin/org/fossify/clock/adapters/TimeZonesAdapter.kt @@ -1,5 +1,6 @@ package org.fossify.clock.adapters +import android.annotation.SuppressLint import android.view.Menu import android.view.View import android.view.ViewGroup @@ -67,12 +68,14 @@ class TimeZonesAdapter(activity: SimpleActivity, var timeZones: ArrayList) { timeZones = newItems notifyDataSetChanged() finishActMode() } + @SuppressLint("NotifyDataSetChanged") fun updateTimes() { notifyDataSetChanged() } diff --git a/app/src/main/kotlin/org/fossify/clock/adapters/TimerAdapter.kt b/app/src/main/kotlin/org/fossify/clock/adapters/TimerAdapter.kt index 3c76a798..5940cff9 100644 --- a/app/src/main/kotlin/org/fossify/clock/adapters/TimerAdapter.kt +++ b/app/src/main/kotlin/org/fossify/clock/adapters/TimerAdapter.kt @@ -1,22 +1,41 @@ package org.fossify.clock.adapters +import android.annotation.SuppressLint import android.view.Menu +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil -import me.grantland.widget.AutofitHelper +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import org.fossify.clock.R import org.fossify.clock.activities.SimpleActivity import org.fossify.clock.databinding.ItemTimerBinding +import org.fossify.clock.extensions.config import org.fossify.clock.extensions.getFormattedDuration import org.fossify.clock.extensions.hideTimerNotification import org.fossify.clock.extensions.secondsToMillis +import org.fossify.clock.extensions.swap import org.fossify.clock.models.Timer import org.fossify.clock.models.TimerEvent -import org.fossify.clock.models.TimerState +import org.fossify.clock.models.TimerState.Finished +import org.fossify.clock.models.TimerState.Idle +import org.fossify.clock.models.TimerState.Paused +import org.fossify.clock.models.TimerState.Running +import org.fossify.commons.adapters.MyRecyclerViewAdapter import org.fossify.commons.adapters.MyRecyclerViewListAdapter import org.fossify.commons.dialogs.PermissionRequiredDialog -import org.fossify.commons.extensions.* +import org.fossify.commons.extensions.adjustAlpha +import org.fossify.commons.extensions.applyColorFilter +import org.fossify.commons.extensions.beInvisibleIf +import org.fossify.commons.extensions.beVisibleIf +import org.fossify.commons.extensions.getColoredDrawableWithColor +import org.fossify.commons.extensions.getFormattedDuration +import org.fossify.commons.extensions.openNotificationSettings +import org.fossify.commons.helpers.SORT_BY_CUSTOM +import org.fossify.commons.interfaces.ItemMoveCallback +import org.fossify.commons.interfaces.ItemTouchHelperContract +import org.fossify.commons.interfaces.StartReorderDragListener import org.fossify.commons.views.MyRecyclerView import org.greenrobot.eventbus.EventBus @@ -25,7 +44,15 @@ class TimerAdapter( recyclerView: MyRecyclerView, onRefresh: () -> Unit, onItemClick: (Timer) -> Unit, -) : MyRecyclerViewListAdapter(simpleActivity, recyclerView, diffUtil, onItemClick, onRefresh) { +) : MyRecyclerViewListAdapter( + activity = simpleActivity, + recyclerView = recyclerView, + diffUtil = diffUtil, + itemClick = onItemClick, + onRefresh = onRefresh +), ItemTouchHelperContract { + + private var startReorderDragListener: StartReorderDragListener companion object { private val diffUtil = object : DiffUtil.ItemCallback() { @@ -41,6 +68,32 @@ class TimerAdapter( init { setupDragListener(true) + setHasStableIds(true) + + val touchHelper = ItemTouchHelper(ItemMoveCallback(this)) + touchHelper.attachToRecyclerView(recyclerView) + startReorderDragListener = object : StartReorderDragListener { + override fun requestDrag(viewHolder: RecyclerView.ViewHolder) { + touchHelper.startDrag(viewHolder) + } + } + } + + override fun getItemId(position: Int): Long { + return getItem(position).id!!.toLong() + } + + override fun submitList(list: MutableList?, commitCallback: Runnable?) { + val layoutManager = recyclerView.layoutManager!! + val recyclerViewState = layoutManager.onSaveInstanceState() + super.submitList(list) { + layoutManager.onRestoreInstanceState(recyclerViewState) + commitCallback?.run() + } + } + + override fun submitList(list: MutableList?) { + submitList(list, null) } override fun getActionMenuId() = R.menu.cab_alarms @@ -74,17 +127,31 @@ class TimerAdapter( return position } - override fun onActionModeCreated() {} + @SuppressLint("NotifyDataSetChanged") + override fun onActionModeCreated() { + notifyDataSetChanged() + } - override fun onActionModeDestroyed() {} + @SuppressLint("NotifyDataSetChanged") + override fun onActionModeDestroyed() { + notifyDataSetChanged() + } + + override fun onRowClear(myViewHolder: MyRecyclerViewAdapter.ViewHolder?) {} + + override fun onRowSelected(myViewHolder: MyRecyclerViewAdapter.ViewHolder?) {} override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return createViewHolder(ItemTimerBinding.inflate(layoutInflater, parent, false).root) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bindView(getItem(position), true, true) { itemView, _ -> - setupView(itemView, getItem(position)) + holder.bindView( + item = getItem(position), + allowSingleClick = true, + allowLongClick = true + ) { itemView, _ -> + setupView(view = itemView, timer = getItem(position), holder = holder) } bindViewHolder(holder) } @@ -98,22 +165,31 @@ class TimerAdapter( timersToRemove.forEach(::deleteTimer) } - private fun setupView(view: View, timer: Timer) { + @SuppressLint("ClickableViewAccessibility") + private fun setupView(view: View, timer: Timer, holder: ViewHolder) { ItemTimerBinding.bind(view).apply { val isSelected = selectedKeys.contains(timer.id) timerFrame.isSelected = isSelected + timerDragHandle.beVisibleIf(selectedKeys.isNotEmpty()) + timerDragHandle.applyColorFilter(textColor) + timerDragHandle.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + startReorderDragListener.requestDrag(holder) + } + false + } timerLabel.setTextColor(textColor) timerLabel.setHintTextColor(textColor.adjustAlpha(0.7f)) timerLabel.text = timer.label + timerLabel.beVisibleIf(timer.label.isNotEmpty()) - AutofitHelper.create(timerTime) timerTime.setTextColor(textColor) timerTime.text = when (timer.state) { - is TimerState.Finished -> 0.getFormattedDuration() - is TimerState.Idle -> timer.seconds.getFormattedDuration() - is TimerState.Paused -> timer.state.tick.getFormattedDuration() - is TimerState.Running -> timer.state.tick.getFormattedDuration() + is Finished -> 0.getFormattedDuration() + is Idle -> timer.seconds.getFormattedDuration() + is Paused -> timer.state.tick.getFormattedDuration() + is Running -> timer.state.tick.getFormattedDuration() } timerReset.applyColorFilter(textColor) @@ -123,32 +199,52 @@ class TimerAdapter( timerPlayPause.applyColorFilter(textColor) timerPlayPause.setOnClickListener { - (activity as SimpleActivity).handleNotificationPermission { granted -> - if (granted) { - when (val state = timer.state) { - is TimerState.Idle -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis)) - is TimerState.Paused -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, state.tick)) - is TimerState.Running -> EventBus.getDefault().post(TimerEvent.Pause(timer.id!!, state.tick)) - is TimerState.Finished -> EventBus.getDefault().post(TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis)) - } - } else { - PermissionRequiredDialog( - activity, - org.fossify.commons.R.string.allow_notifications_reminders, - { activity.openNotificationSettings() }) - } - } + toggleTimer(timer) } val state = timer.state - val resetPossible = state is TimerState.Running || state is TimerState.Paused || state is TimerState.Finished + val resetPossible = state is Running || state is Paused || state is Finished timerReset.beInvisibleIf(!resetPossible) - val drawableId = if (state is TimerState.Running) { - org.fossify.commons.R.drawable.ic_pause_vector + timerPlayPause.setImageDrawable( + simpleActivity.resources.getColoredDrawableWithColor( + drawableId = if (state is Running) { + org.fossify.commons.R.drawable.ic_pause_vector + } else { + org.fossify.commons.R.drawable.ic_play_vector + }, + color = textColor + ) + ) + } + } + + private fun toggleTimer(timer: Timer) { + (activity as SimpleActivity).handleNotificationPermission { granted -> + if (granted) { + when (val state = timer.state) { + is Idle -> EventBus.getDefault().post( + TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis) + ) + + is Paused -> EventBus.getDefault().post( + TimerEvent.Start(timer.id!!, state.tick) + ) + + is Running -> EventBus.getDefault().post( + TimerEvent.Pause(timer.id!!, state.tick) + ) + + is Finished -> EventBus.getDefault().post( + TimerEvent.Start(timer.id!!, timer.seconds.secondsToMillis) + ) + } } else { - org.fossify.commons.R.drawable.ic_play_vector + PermissionRequiredDialog( + activity = activity, + textId = org.fossify.commons.R.string.allow_notifications_reminders, + positiveActionCallback = { activity.openNotificationSettings() } + ) } - timerPlayPause.setImageDrawable(simpleActivity.resources.getColoredDrawableWithColor(drawableId, textColor)) } } @@ -161,4 +257,20 @@ class TimerAdapter( EventBus.getDefault().post(TimerEvent.Delete(timer.id!!)) simpleActivity.hideTimerNotification(timer.id!!) } + + override fun onRowMoved(fromPosition: Int, toPosition: Int) { + val timers = currentList.toMutableList() + timers.swap(fromPosition, toPosition) + submitList(timers) + saveAlarmsCustomOrder(ArrayList(timers)) + if (simpleActivity.config.timerSort != SORT_BY_CUSTOM) { + simpleActivity.config.timerSort = SORT_BY_CUSTOM + } + } + + private fun saveAlarmsCustomOrder(alarms: ArrayList) { + val timersCustomSortingIds = alarms.map { it.id } + simpleActivity.config.timersCustomSorting = + timersCustomSortingIds.joinToString { it.toString() } + } } diff --git a/app/src/main/kotlin/org/fossify/clock/adapters/ViewPagerAdapter.kt b/app/src/main/kotlin/org/fossify/clock/adapters/ViewPagerAdapter.kt index d5cb5fba..80547d80 100644 --- a/app/src/main/kotlin/org/fossify/clock/adapters/ViewPagerAdapter.kt +++ b/app/src/main/kotlin/org/fossify/clock/adapters/ViewPagerAdapter.kt @@ -45,6 +45,10 @@ class ViewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { (fragments[TAB_ALARM_INDEX] as? AlarmFragment)?.showSortingDialog() } + fun showTimerSortDialog() { + (fragments[TAB_TIMER_INDEX] as? TimerFragment)?.showSortingDialog() + } + fun updateClockTabAlarm() { (fragments[TAB_CLOCK_INDEX] as? ClockFragment)?.updateAlarm() } diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ChangeAlarmSortDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ChangeAlarmSortDialog.kt index 073d00e3..efd25852 100644 --- a/app/src/main/kotlin/org/fossify/clock/dialogs/ChangeAlarmSortDialog.kt +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ChangeAlarmSortDialog.kt @@ -9,12 +9,14 @@ import org.fossify.clock.helpers.SORT_BY_DATE_AND_TIME import org.fossify.commons.activities.BaseSimpleActivity import org.fossify.commons.extensions.getAlertDialogBuilder import org.fossify.commons.extensions.setupDialogStuff +import org.fossify.commons.helpers.SORT_BY_CUSTOM class ChangeAlarmSortDialog(val activity: BaseSimpleActivity, val callback: () -> Unit) { private val binding = DialogChangeAlarmSortBinding.inflate(activity.layoutInflater).apply { val activeRadioButton = when (activity.config.alarmSort) { SORT_BY_ALARM_TIME -> sortingDialogRadioAlarmTime SORT_BY_DATE_AND_TIME -> sortingDialogRadioDayAndTime + SORT_BY_CUSTOM -> sortingDialogRadioCustom else -> sortingDialogRadioCreationOrder } activeRadioButton.isChecked = true @@ -33,6 +35,7 @@ class ChangeAlarmSortDialog(val activity: BaseSimpleActivity, val callback: () - val sort = when (binding.sortingDialogRadioSorting.checkedRadioButtonId) { R.id.sorting_dialog_radio_alarm_time -> SORT_BY_ALARM_TIME R.id.sorting_dialog_radio_day_and_time -> SORT_BY_DATE_AND_TIME + R.id.sorting_dialog_radio_custom -> SORT_BY_CUSTOM else -> SORT_BY_CREATION_ORDER } diff --git a/app/src/main/kotlin/org/fossify/clock/dialogs/ChangeTimerSortDialog.kt b/app/src/main/kotlin/org/fossify/clock/dialogs/ChangeTimerSortDialog.kt new file mode 100644 index 00000000..6d81bd01 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/dialogs/ChangeTimerSortDialog.kt @@ -0,0 +1,42 @@ +package org.fossify.clock.dialogs + +import org.fossify.clock.R +import org.fossify.clock.databinding.DialogChangeTimerSortBinding +import org.fossify.clock.extensions.config +import org.fossify.clock.helpers.SORT_BY_CREATION_ORDER +import org.fossify.clock.helpers.SORT_BY_TIMER_DURATION +import org.fossify.commons.activities.BaseSimpleActivity +import org.fossify.commons.extensions.getAlertDialogBuilder +import org.fossify.commons.extensions.setupDialogStuff +import org.fossify.commons.helpers.SORT_BY_CUSTOM + +class ChangeTimerSortDialog(val activity: BaseSimpleActivity, val callback: () -> Unit) { + private val binding = DialogChangeTimerSortBinding.inflate(activity.layoutInflater).apply { + val activeRadioButton = when (activity.config.timerSort) { + SORT_BY_TIMER_DURATION -> sortingDialogRadioTimerDuration + SORT_BY_CUSTOM -> sortingDialogRadioCustom + else -> sortingDialogRadioCreationOrder + } + activeRadioButton.isChecked = true + } + + init { + activity.getAlertDialogBuilder() + .setPositiveButton(org.fossify.commons.R.string.ok) { _, _ -> dialogConfirmed() } + .setNegativeButton(org.fossify.commons.R.string.cancel, null) + .apply { + activity.setupDialogStuff(binding.root, this, org.fossify.commons.R.string.sort_by) + } + } + + private fun dialogConfirmed() { + val sort = when (binding.sortingDialogRadioSorting.checkedRadioButtonId) { + R.id.sorting_dialog_radio_timer_duration -> SORT_BY_TIMER_DURATION + R.id.sorting_dialog_radio_custom -> SORT_BY_CUSTOM + else -> SORT_BY_CREATION_ORDER + } + + activity.config.timerSort = sort + callback() + } +} diff --git a/app/src/main/kotlin/org/fossify/clock/extensions/MutableList.kt b/app/src/main/kotlin/org/fossify/clock/extensions/MutableList.kt new file mode 100644 index 00000000..3937e0d4 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/extensions/MutableList.kt @@ -0,0 +1,14 @@ +package org.fossify.clock.extensions + +@Deprecated( + message = "Use the `move` extension function from commons", + replaceWith = ReplaceWith( + expression = "this.move(index1, index2)", + imports = ["org.fossify.commons.extensions.move"] + ) +) +fun MutableList.swap(index1: Int, index2: Int) { + this[index1] = this[index2].also { + this[index2] = this[index1] + } +} diff --git a/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt b/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt index de83517e..6b8c516d 100644 --- a/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt +++ b/app/src/main/kotlin/org/fossify/clock/fragments/AlarmFragment.kt @@ -20,6 +20,7 @@ import org.fossify.commons.extensions.getProperBackgroundColor import org.fossify.commons.extensions.getProperTextColor import org.fossify.commons.extensions.toast import org.fossify.commons.extensions.updateTextColors +import org.fossify.commons.helpers.SORT_BY_CUSTOM import org.fossify.commons.helpers.SORT_BY_DATE_CREATED import org.fossify.commons.helpers.ensureBackgroundThread import org.fossify.commons.models.AlarmSound @@ -84,6 +85,25 @@ class AlarmFragment : Fragment(), ToggleAlarmInterface { }.thenBy { it.timeInMinutes }) + + SORT_BY_CUSTOM -> { + val customAlarmsSortOrderString = activity?.config?.alarmsCustomSorting + if (customAlarmsSortOrderString == "") { + alarms.sortBy { it.id } + } else { + val customAlarmsSortOrder: List = customAlarmsSortOrderString?.split(", ")?.map { it.toInt() }!! + val alarmsIdValueMap = alarms.associateBy { it.id } + + val sortedAlarms: ArrayList = ArrayList() + customAlarmsSortOrder.map { id -> + if (alarmsIdValueMap[id] != null) { + sortedAlarms.add(alarmsIdValueMap[id] as Alarm) + } + } + + alarms = (sortedAlarms + alarms.filter { it !in sortedAlarms }) as ArrayList + } + } } context?.getEnabledAlarms { enabledAlarms -> if (enabledAlarms.isNullOrEmpty()) { diff --git a/app/src/main/kotlin/org/fossify/clock/fragments/TimerFragment.kt b/app/src/main/kotlin/org/fossify/clock/fragments/TimerFragment.kt index 7c449b7b..d7a2365b 100644 --- a/app/src/main/kotlin/org/fossify/clock/fragments/TimerFragment.kt +++ b/app/src/main/kotlin/org/fossify/clock/fragments/TimerFragment.kt @@ -10,24 +10,27 @@ import androidx.fragment.app.Fragment import org.fossify.clock.activities.SimpleActivity import org.fossify.clock.adapters.TimerAdapter import org.fossify.clock.databinding.FragmentTimerBinding +import org.fossify.clock.dialogs.ChangeTimerSortDialog import org.fossify.clock.dialogs.EditTimerDialog import org.fossify.clock.extensions.config import org.fossify.clock.extensions.createNewTimer import org.fossify.clock.extensions.timerHelper import org.fossify.clock.helpers.DisabledItemChangeAnimator +import org.fossify.clock.helpers.SORT_BY_TIMER_DURATION import org.fossify.clock.models.Timer import org.fossify.clock.models.TimerEvent import org.fossify.commons.extensions.getProperBackgroundColor import org.fossify.commons.extensions.getProperTextColor import org.fossify.commons.extensions.hideKeyboard import org.fossify.commons.extensions.updateTextColors +import org.fossify.commons.helpers.SORT_BY_CUSTOM +import org.fossify.commons.helpers.SORT_BY_DATE_CREATED import org.fossify.commons.models.AlarmSound import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode class TimerFragment : Fragment() { - private val INVALID_POSITION = -1 private lateinit var binding: FragmentTimerBinding private lateinit var timerAdapter: TimerAdapter private var timerPositionToScrollTo = INVALID_POSITION @@ -43,7 +46,11 @@ class TimerFragment : Fragment() { super.onDestroy() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { binding = FragmentTimerBinding.inflate(inflater, container, false).apply { timersList.itemAnimator = DisabledItemChangeAnimator() timerAdd.setOnClickListener { @@ -73,7 +80,12 @@ class TimerFragment : Fragment() { timerAdapter.updateBackgroundColor(requireContext().getProperBackgroundColor()) timerAdapter.updateTextColor(requireContext().getProperTextColor()) } else { - timerAdapter = TimerAdapter(requireActivity() as SimpleActivity, binding.timersList, ::refreshTimers, ::openEditTimer) + timerAdapter = TimerAdapter( + simpleActivity = requireActivity() as SimpleActivity, + recyclerView = binding.timersList, + onRefresh = ::refreshTimers, + onItemClick = ::openEditTimer + ) binding.timersList.adapter = timerAdapter } } @@ -85,16 +97,67 @@ class TimerFragment : Fragment() { refreshTimers() } - private fun refreshTimers(scrollToLatest: Boolean = false) { + fun showSortingDialog() { + ChangeTimerSortDialog(activity as SimpleActivity) { + refreshTimers( + animate = false // disable sorting animations for now. + ) + } + } + + private fun getSortedTimers(callback: (List) -> Unit) { activity?.timerHelper?.getTimers { timers -> + val sortedTimers = when (requireContext().config.timerSort) { + SORT_BY_TIMER_DURATION -> timers.sortedBy { it.seconds } + SORT_BY_DATE_CREATED -> timers.sortedBy { it.id } + SORT_BY_CUSTOM -> { + val customTimersSortOrderString = activity?.config?.timersCustomSorting + if (customTimersSortOrderString == "") { + timers.sortedBy { it.id } + } else { + val customTimersSortOrder = + customTimersSortOrderString?.split(", ")?.map { it.toInt() }!! + val timersIdValueMap = timers.associateBy { it.id } + + val sortedTimers: ArrayList = ArrayList() + customTimersSortOrder.map { id -> + if (timersIdValueMap[id] != null) { + sortedTimers.add(timersIdValueMap[id] as Timer) + } + } + + (sortedTimers + timers.filter { it !in sortedTimers }) as ArrayList + } + } + + else -> timers + } + activity?.runOnUiThread { - timerAdapter.submitList(timers) { + callback(sortedTimers) + } + } + } + + private fun refreshTimers(animate: Boolean = true) { + getSortedTimers { timers -> + with(binding.timersList) { + val originalAnimator = itemAnimator + if (!animate) { + itemAnimator = null + } + + timerAdapter.submitList(timers.toMutableList()) { view?.post { - if (timerPositionToScrollTo != INVALID_POSITION && timerAdapter.itemCount > timerPositionToScrollTo) { - binding.timersList.scrollToPosition(timerPositionToScrollTo) + if (timerPositionToScrollTo != INVALID_POSITION && + timerAdapter.itemCount > timerPositionToScrollTo + ) { + smoothScrollToPosition(timerPositionToScrollTo) timerPositionToScrollTo = INVALID_POSITION - } else if (scrollToLatest) { - binding.timersList.scrollToPosition(timers.lastIndex) + } + + if (!animate) { + itemAnimator = originalAnimator } } } @@ -112,15 +175,13 @@ class TimerFragment : Fragment() { } fun updatePosition(timerId: Int) { - activity?.timerHelper?.getTimers { timers -> + getSortedTimers { timers -> val position = timers.indexOfFirst { it.id == timerId } if (position != INVALID_POSITION) { - activity?.runOnUiThread { - if (timerAdapter.itemCount > position) { - binding.timersList.scrollToPosition(position) - } else { - timerPositionToScrollTo = position - } + if (timerAdapter.itemCount > position) { + binding.timersList.smoothScrollToPosition(position) + } else { + timerPositionToScrollTo = position } } } @@ -133,3 +194,5 @@ class TimerFragment : Fragment() { } } } + +private const val INVALID_POSITION = -1 diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt index 873006a7..9a545bea 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Config.kt @@ -59,6 +59,18 @@ class Config(context: Context) : BaseConfig(context) { get() = prefs.getInt(ALARMS_SORT_BY, SORT_BY_CREATION_ORDER) set(alarmSort) = prefs.edit().putInt(ALARMS_SORT_BY, alarmSort).apply() + var alarmsCustomSorting: String + get() = prefs.getString(ALARMS_CUSTOM_SORTING, "")!! + set(alarmsCustomSorting) = prefs.edit().putString(ALARMS_CUSTOM_SORTING, alarmsCustomSorting).apply() + + var timerSort: Int + get() = prefs.getInt(TIMERS_SORT_BY, SORT_BY_CREATION_ORDER) + set(timerSort) = prefs.edit().putInt(TIMERS_SORT_BY, timerSort).apply() + + var timersCustomSorting: String + get() = prefs.getString(TIMERS_CUSTOM_SORTING, "")!! + set(timersCustomSorting) = prefs.edit().putString(TIMERS_CUSTOM_SORTING, timersCustomSorting).apply() + var alarmMaxReminderSecs: Int get() = prefs.getInt(ALARM_MAX_REMINDER_SECS, DEFAULT_MAX_ALARM_REMINDER_SECS) set(alarmMaxReminderSecs) = prefs.edit().putInt(ALARM_MAX_REMINDER_SECS, alarmMaxReminderSecs).apply() diff --git a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt index 53772ba1..64ad9ade 100644 --- a/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/clock/helpers/Constants.kt @@ -24,6 +24,9 @@ const val ALARM_LAST_CONFIG = "alarm_last_config" const val TIMER_LAST_CONFIG = "timer_last_config" const val INCREASE_VOLUME_GRADUALLY = "increase_volume_gradually" const val ALARMS_SORT_BY = "alarms_sort_by" +const val ALARMS_CUSTOM_SORTING = "alarms_custom_sorting" +const val TIMERS_SORT_BY = "timers_sort_by" +const val TIMERS_CUSTOM_SORTING = "timers_custom_sorting" const val STOPWATCH_LAPS_SORT_BY = "stopwatch_laps_sort_by" const val WAS_INITIAL_WIDGET_SET_UP = "was_initial_widget_set_up" const val DATA_EXPORT_EXTENSION = ".json" @@ -70,10 +73,11 @@ const val SORT_BY_LAP = 1 const val SORT_BY_LAP_TIME = 2 const val SORT_BY_TOTAL_TIME = 4 -// alarm sorting +// alarm and timer sorting const val SORT_BY_CREATION_ORDER = 0 const val SORT_BY_ALARM_TIME = 1 const val SORT_BY_DATE_AND_TIME = 2 +const val SORT_BY_TIMER_DURATION = 3 const val TODAY_BIT = -1 const val TOMORROW_BIT = -2 @@ -188,6 +192,7 @@ fun getAllTimeZones() = arrayListOf( MyTimeZone(28, "GMT-01:00 Cape Verde", "Atlantic/Cape_Verde"), MyTimeZone(29, "GMT+00:00 Casablanca", "Africa/Casablanca"), MyTimeZone(30, "GMT+00:00 Greenwich Mean Time", "Etc/Greenwich"), + MyTimeZone(90, "GMT+00:00 London", "Europe/London"), MyTimeZone(31, "GMT+01:00 Amsterdam", "Europe/Amsterdam"), MyTimeZone(32, "GMT+01:00 Belgrade", "Europe/Belgrade"), MyTimeZone(33, "GMT+01:00 Brussels", "Europe/Brussels"), diff --git a/app/src/main/kotlin/org/fossify/clock/views/AutoFitTextView.kt b/app/src/main/kotlin/org/fossify/clock/views/AutoFitTextView.kt new file mode 100644 index 00000000..7135d0d2 --- /dev/null +++ b/app/src/main/kotlin/org/fossify/clock/views/AutoFitTextView.kt @@ -0,0 +1,47 @@ +package org.fossify.clock.views + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.util.AttributeSet +import android.util.TypedValue +import android.widget.TextView +import androidx.annotation.RequiresApi + +/** + * A simple wrapper TextView that restores the original text size + * when view width is restored. + */ +@SuppressLint("AppCompatCustomView") +@RequiresApi(Build.VERSION_CODES.O) +class AutoFitTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0, +) : TextView(context, attrs, defStyle) { + + private var originalTextSize: Float = textSize + private var originalWidth: Int = 0 + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + if (originalWidth == 0) { + originalWidth = w + } + + post { + if (w >= originalWidth && textSize != originalTextSize) { + disableAutoSizing() + setTextSize(TypedValue.COMPLEX_UNIT_PX, originalTextSize) + enableAutoSizing() + } + } + } + + private fun disableAutoSizing() { + setAutoSizeTextTypeWithDefaults(AUTO_SIZE_TEXT_TYPE_NONE) + } + + private fun enableAutoSizing() { + post { setAutoSizeTextTypeWithDefaults(AUTO_SIZE_TEXT_TYPE_UNIFORM) } + } +} diff --git a/app/src/main/res/layout/dialog_change_alarm_sort.xml b/app/src/main/res/layout/dialog_change_alarm_sort.xml index 41d9db1d..f4cce66d 100644 --- a/app/src/main/res/layout/dialog_change_alarm_sort.xml +++ b/app/src/main/res/layout/dialog_change_alarm_sort.xml @@ -37,5 +37,13 @@ android:paddingBottom="@dimen/medium_margin" android:text="@string/sort_by_day_and_alarm_time" /> + + diff --git a/app/src/main/res/layout/dialog_change_timer_sort.xml b/app/src/main/res/layout/dialog_change_timer_sort.xml new file mode 100644 index 00000000..9be36fa5 --- /dev/null +++ b/app/src/main/res/layout/dialog_change_timer_sort.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_alarm.xml b/app/src/main/res/layout/item_alarm.xml index 9edabf0e..1ef82317 100644 --- a/app/src/main/res/layout/item_alarm.xml +++ b/app/src/main/res/layout/item_alarm.xml @@ -59,7 +59,20 @@ android:layout_height="wrap_content" android:paddingHorizontal="@dimen/normal_margin" app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/alarm_drag_handle" + app:layout_constraintStart_toEndOf="@id/alarm_time" + app:layout_constraintTop_toTopOf="parent" /> + + diff --git a/app/src/main/res/layout/item_timer.xml b/app/src/main/res/layout/item_timer.xml index e2b3c5d5..96c90298 100644 --- a/app/src/main/res/layout/item_timer.xml +++ b/app/src/main/res/layout/item_timer.xml @@ -1,5 +1,5 @@ - + android:foreground="@drawable/selector" + android:paddingHorizontal="@dimen/activity_margin" + android:paddingVertical="@dimen/normal_margin"> - + android:autoSizeMaxTextSize="@dimen/timer_text_size" + android:autoSizeMinTextSize="@dimen/extra_big_text_size" + android:autoSizeStepGranularity="2sp" + android:autoSizeTextType="uniform" + android:gravity="center_vertical" + android:includeFontPadding="false" + android:maxLines="1" + android:textSize="@dimen/timer_text_size" + app:layout_constraintBottom_toTopOf="@id/timer_label" + app:layout_constraintEnd_toStartOf="@id/timer_reset" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="01:30:00" /> - + - + - + - + - - + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index a6255c42..bdd457c1 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -11,10 +11,12 @@ 50dp 56dp 24dp + 40dp 70sp 60sp 60sp + @dimen/alarm_text_size 80sp 48sp 14sp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5df0ed1b..5990fe5e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,6 +18,7 @@ Swipe right to Dismiss, or left to Snooze. Creation order Alarm time + Timer duration Day and Alarm time Analogue clock Digital clock