Add sort by list

pull/2282/head
Alex Baker 1 year ago
parent 13b4f80c70
commit 18242e5284

@ -17,6 +17,7 @@ import com.todoroo.andlib.sql.Functions;
import com.todoroo.andlib.sql.Order;
import com.todoroo.astrid.data.Task;
import org.tasks.data.CaldavCalendar;
import org.tasks.preferences.QueryPreferences;
import java.util.Locale;
@ -37,6 +38,7 @@ public class SortHelper {
public static final int SORT_GTASKS = 6;
public static final int SORT_CALDAV = 7;
public static final int SORT_START = 8;
public static final int SORT_LIST = 9;
public static final long APPLE_EPOCH = 978307200000L; // 1/1/2001 GMT
@SuppressLint("DefaultLocale")
@ -48,6 +50,7 @@ public class SortHelper {
private static final String ADJUSTED_START_DATE =
"(CASE WHEN (hideUntil / 1000) % 60 > 0 THEN hideUntil ELSE (hideUntil + 86399000) END)";
private static final Order ORDER_TITLE = Order.asc(Functions.upper(Task.TITLE));
private static final Order ORDER_LIST = Order.asc(Functions.upper(CaldavCalendar.NAME));
/** Takes a SQL query, and if there isn't already an order, creates an order. */
public static String adjustQueryForFlagsAndSort(
@ -113,6 +116,9 @@ public class SortHelper {
case SORT_CREATED:
order = Order.desc(Task.CREATION_DATE);
break;
case SORT_LIST:
order = ORDER_LIST;
break;
default:
order =
Order.asc(
@ -146,6 +152,8 @@ public class SortHelper {
return "tasks.modified";
case SORT_CREATED:
return "tasks.created";
case SORT_LIST:
return "cdl_id";
default:
return null;
}
@ -168,6 +176,7 @@ public class SortHelper {
case SORT_CREATED -> "tasks.created";
case SORT_GTASKS -> "tasks.`order`";
case SORT_CALDAV -> CALDAV_ORDER_COLUMN;
case SORT_LIST -> "cdl_name";
default -> "(CASE WHEN (tasks.dueDate=0) "
+ // if no due date
"THEN (strftime('%s','now')*1000)*2 "

@ -51,6 +51,6 @@ data class CaldavCalendar(
val TABLE = Table("caldav_lists")
val ACCOUNT = TABLE.column("cdl_account")
val UUID = TABLE.column("cdl_uuid")
val NAME = TABLE.column("cdl_name")
@JvmField val NAME = TABLE.column("cdl_name")
}
}

@ -33,6 +33,9 @@ abstract class CaldavDao {
@Query("SELECT * FROM caldav_lists WHERE cdl_uuid = :uuid LIMIT 1")
abstract suspend fun getCalendarByUuid(uuid: String): CaldavCalendar?
@Query("SELECT * FROM caldav_lists WHERE cdl_id = :id LIMIT 1")
abstract suspend fun getCalendarById(id: Long): CaldavCalendar?
@Query("SELECT * FROM caldav_lists WHERE cdl_account = :uuid")
abstract suspend fun getCalendarsByAccount(uuid: String): List<CaldavCalendar>

@ -48,14 +48,22 @@ internal object TaskListQueryRecursive {
else -> PermaSql.replacePlaceholdersForQuery(filter.getSqlQuery())
}
val manualSort = preferences.isManualSort
val sortPreference = preferences.sortMode
val sortMode = when {
manualSort && filter is GtasksFilter -> SortHelper.SORT_GTASKS
manualSort && filter is CaldavFilter -> SortHelper.SORT_CALDAV
else -> preferences.sortMode
sortPreference == SortHelper.SORT_LIST && (filter is GtasksFilter || filter is CaldavFilter) ->
SortHelper.SORT_AUTO
else -> sortPreference
}
val reverseSort =
preferences.isReverseSort && sortMode != SortHelper.SORT_GTASKS && sortMode != SortHelper.SORT_CALDAV
val sortSelect = SortHelper.orderSelectForSortTypeRecursive(sortMode)
val primarySortSelect = SortHelper.orderSelectForSortTypeRecursive(sortMode)
val secondarySortSelect = if (sortMode == SortHelper.SORT_LIST) {
"NULL"
} else {
primarySortSelect
}
val parentCompleted = if (preferences.completedTasksAtBottom) "tasks.completed > 0" else "0"
val completionSort =
if (preferences.completedTasksAtBottom && preferences.sortCompletedByCompletionDate) {
@ -66,10 +74,20 @@ internal object TaskListQueryRecursive {
val withClause = """
CREATE TEMPORARY TABLE `recursive_tasks` AS
WITH RECURSIVE recursive_tasks (task, parent_complete, subtask_complete, completion_sort, parent, collapsed, hidden, indent, title, primary_sort, secondary_sort, sort_group) AS (
SELECT tasks._id, $parentCompleted as parent_complete, 0 as subtask_complete, $completionSort as completion_sort, 0 as parent, tasks.collapsed as collapsed, 0 as hidden, 0 AS sort_indent, UPPER(tasks.title) AS sort_title, $sortSelect as primary_sort, NULL as secondarySort, ${SortHelper.getSortGroup(sortMode)}
SELECT tasks._id, $parentCompleted as parent_complete, 0 as subtask_complete, $completionSort as completion_sort, 0 as parent, tasks.collapsed as collapsed, 0 as hidden, 0 AS sort_indent, UPPER(tasks.title) AS sort_title, $primarySortSelect as primary_sort, NULL as secondarySort, ${SortHelper.getSortGroup(sortMode)}
FROM tasks
${
if (sortMode == SortHelper.SORT_LIST) {
"""
LEFT JOIN caldav_tasks on cd_task = tasks._id AND cd_deleted = 0
LEFT JOIN caldav_lists on cd_calendar = cdl_uuid
""".trimIndent()
} else {
""
}
}
$parentQuery
UNION ALL SELECT tasks._id, recursive_tasks.parent_complete, $parentCompleted as subtask_complete, $completionSort as completion_sort, recursive_tasks.task as parent, tasks.collapsed as collapsed, CASE WHEN recursive_tasks.collapsed > 0 OR recursive_tasks.hidden > 0 THEN 1 ELSE 0 END as hidden, recursive_tasks.indent+1 AS sort_indent, UPPER(tasks.title) AS sort_title, recursive_tasks.primary_sort as primary_sort, $sortSelect as secondary_sort, recursive_tasks.sort_group FROM tasks
UNION ALL SELECT tasks._id, recursive_tasks.parent_complete, $parentCompleted as subtask_complete, $completionSort as completion_sort, recursive_tasks.task as parent, tasks.collapsed as collapsed, CASE WHEN recursive_tasks.collapsed > 0 OR recursive_tasks.hidden > 0 THEN 1 ELSE 0 END as hidden, recursive_tasks.indent+1 AS sort_indent, UPPER(tasks.title) AS sort_title, recursive_tasks.primary_sort as primary_sort, $secondarySortSelect as secondary_sort, recursive_tasks.sort_group FROM tasks
$SUBTASK_QUERY
ORDER BY parent_complete ASC, sort_indent DESC, subtask_complete ASC, completion_sort DESC, ${SortHelper.orderForSortTypeRecursive(sortMode, reverseSort)}
) SELECT * FROM recursive_tasks

@ -7,20 +7,26 @@ import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.core.SortHelper;
import dagger.hilt.android.AndroidEntryPoint;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.tasks.R;
import org.tasks.preferences.Preferences;
import org.tasks.preferences.QueryPreferences;
import org.tasks.widget.WidgetPreferences;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import timber.log.Timber;
@AndroidEntryPoint
@ -92,6 +98,7 @@ public class SortDialog extends DialogFragment {
items.add(getString(R.string.SSD_sort_alpha));
items.add(getString(R.string.SSD_sort_modified));
items.add(getString(R.string.sort_created));
items.add(getString(R.string.sort_list));
if (manualEnabled) {
if (preferences.isManualSort()) {
@ -186,6 +193,8 @@ public class SortDialog extends DialogFragment {
return 6;
case SortHelper.SORT_CREATED:
return 7;
case SortHelper.SORT_LIST:
return 8;
}
Timber.e("Invalid sort mode: %s", sortMode);
@ -208,6 +217,8 @@ public class SortDialog extends DialogFragment {
return SortHelper.SORT_MODIFIED;
case 7:
return SortHelper.SORT_CREATED;
case 8:
return SortHelper.SORT_LIST;
}
Timber.e("Invalid sort mode: %s", index);

@ -1,16 +1,10 @@
package org.tasks.tasklist
import android.content.Context
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.core.SortHelper.*
import com.todoroo.astrid.core.SortHelper.SORT_START
import org.tasks.R
import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.tasklist.SectionedDataSource.Companion.HEADER_COMPLETED
import org.tasks.tasklist.SectionedDataSource.Companion.HEADER_OVERDUE
import java.time.format.FormatStyle
import java.util.*
data class AdapterSection(
var firstPosition: Int,
@ -26,48 +20,4 @@ data class AdapterSection(
} else {
textColor
})
fun headerString(
context: Context,
locale: Locale,
sortMode: Int,
alwaysDisplayFullDate: Boolean,
style: FormatStyle = FormatStyle.FULL,
compact: Boolean = false
): String =
when {
value == HEADER_COMPLETED -> context.getString(R.string.completed)
sortMode == SORT_IMPORTANCE -> context.getString(priorityToString())
value == HEADER_OVERDUE -> context.getString(R.string.filter_overdue)
value == 0L -> context.getString(when (sortMode) {
SORT_DUE -> R.string.no_due_date
SORT_START -> R.string.no_start_date
else -> R.string.no_date
})
else -> {
val dateString = DateUtilities.getRelativeDay(
context, value, locale, style, alwaysDisplayFullDate, !compact
)
when {
compact -> dateString
sortMode == SORT_DUE ->
context.getString(R.string.sort_due_group, dateString)
sortMode == SORT_START ->
context.getString(R.string.sort_start_group, dateString)
sortMode == SORT_CREATED ->
context.getString(R.string.sort_created_group, dateString)
sortMode == SORT_MODIFIED ->
context.getString(R.string.sort_modified_group, dateString)
else -> throw IllegalArgumentException()
}
}
}
@StringRes
private fun priorityToString() = when (value) {
0L -> R.string.filter_high_priority
1L -> R.string.filter_medium_priority
2L -> R.string.filter_low_priority
else -> R.string.filter_no_priority
}
}

@ -4,8 +4,13 @@ import android.graphics.Canvas
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.*
import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_DRAG
import androidx.recyclerview.widget.ItemTouchHelper.Callback
import androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.LEFT
import androidx.recyclerview.widget.ItemTouchHelper.RIGHT
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.RecyclerView
import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.adapter.TaskAdapter
@ -18,7 +23,8 @@ import kotlinx.coroutines.runBlocking
import org.tasks.activities.DragAndDropDiffer
import org.tasks.data.TaskContainer
import org.tasks.preferences.Preferences
import java.util.*
import java.util.LinkedList
import java.util.Queue
import java.util.concurrent.Executors
import kotlin.math.max
import kotlin.math.min
@ -50,7 +56,7 @@ class DragAndDropRecyclerAdapter(
val viewType = getItemViewType(position)
if (viewType == 1) {
val headerSection = items.getSection(position)
(holder as HeaderViewHolder).bind(taskList.getFilter(), preferences.sortMode, preferences.alwaysDisplayFullDate, headerSection)
(holder as HeaderViewHolder).bind(taskList.getFilter(), preferences.sortMode, headerSection)
} else {
super.onBindViewHolder(holder, position)
}

@ -0,0 +1,77 @@
package org.tasks.tasklist
import android.content.Context
import androidx.annotation.StringRes
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.core.SortHelper
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.preferences.Preferences
import java.time.format.FormatStyle
import java.util.Locale
import javax.inject.Inject
class HeaderFormatter @Inject constructor(
@ApplicationContext private val context: Context,
private val preferences: Preferences,
private val locale: Locale,
private val caldavDao: CaldavDao,
) {
private val listCache = HashMap<Long, String?>()
fun headerStringBlocking(
value: Long,
sortMode: Int = preferences.sortMode,
alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate,
style: FormatStyle = FormatStyle.FULL,
compact: Boolean = false,
) = runBlocking {
headerString(value, sortMode, alwaysDisplayFullDate, style, compact)
}
suspend fun headerString(
value: Long,
sortMode: Int = preferences.sortMode,
alwaysDisplayFullDate: Boolean = preferences.alwaysDisplayFullDate,
style: FormatStyle = FormatStyle.FULL,
compact: Boolean = false
): String =
when {
value == SectionedDataSource.HEADER_COMPLETED -> context.getString(R.string.completed)
sortMode == SortHelper.SORT_IMPORTANCE -> context.getString(priorityToString(value))
sortMode == SortHelper.SORT_LIST ->
listCache.getOrPut(value) { caldavDao.getCalendarById(value)?.name }?: "list: $value"
value == SectionedDataSource.HEADER_OVERDUE -> context.getString(R.string.filter_overdue)
value == 0L -> context.getString(when (sortMode) {
SortHelper.SORT_DUE -> R.string.no_due_date
SortHelper.SORT_START -> R.string.no_start_date
else -> R.string.no_date
})
else -> {
val dateString = DateUtilities.getRelativeDay(
context, value, locale, style, alwaysDisplayFullDate, !compact
)
when {
compact -> dateString
sortMode == SortHelper.SORT_DUE ->
context.getString(R.string.sort_due_group, dateString)
sortMode == SortHelper.SORT_START ->
context.getString(R.string.sort_start_group, dateString)
sortMode == SortHelper.SORT_CREATED ->
context.getString(R.string.sort_created_group, dateString)
sortMode == SortHelper.SORT_MODIFIED ->
context.getString(R.string.sort_modified_group, dateString)
else -> throw IllegalArgumentException()
}
}
}
@StringRes
private fun priorityToString(value: Long) = when (value) {
0L -> R.string.filter_high_priority
1L -> R.string.filter_medium_priority
2L -> R.string.filter_low_priority
else -> R.string.filter_no_priority
}
}

@ -7,11 +7,10 @@ import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.todoroo.astrid.api.Filter
import org.tasks.R
import java.util.*
class HeaderViewHolder(
private val context: Context,
private val locale: Locale,
private val headerFormatter: HeaderFormatter,
view: View,
callback: (Long) -> Unit
) : RecyclerView.ViewHolder(view) {
@ -21,10 +20,10 @@ class HeaderViewHolder(
private var sortGroup = -1L
private var rotation = 0f
fun bind(filter: Filter, sortMode: Int, alwaysDisplayFullDate: Boolean, section: AdapterSection) {
fun bind(filter: Filter, sortMode: Int, section: AdapterSection) {
sortGroup = section.value
val header = if (filter.supportsSorting()) {
section.headerString(context, locale, sortMode, alwaysDisplayFullDate)
headerFormatter.headerStringBlocking(section.value)
} else {
null
}

@ -66,7 +66,11 @@ class SectionedDataSource constructor(
HEADER_COMPLETED
} else if (sortGroup == null) {
continue
} else if (sortMode == SortHelper.SORT_IMPORTANCE || sortGroup == 0L) {
} else if (
sortMode == SortHelper.SORT_LIST ||
sortMode == SortHelper.SORT_IMPORTANCE ||
sortGroup == 0L
) {
sortGroup
} else if (sortMode == SortHelper.SORT_DUE) {
when {
@ -89,6 +93,7 @@ class SectionedDataSource constructor(
sections.add(AdapterSection(i, header, 0, isCollapsed))
}
}
sortMode == SortHelper.SORT_LIST ||
sortMode == SortHelper.SORT_IMPORTANCE ->
if (header != previous) {
sections.add(AdapterSection(i, header, 0, isCollapsed))

@ -17,6 +17,7 @@ import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.core.SortHelper.SORT_DUE
import com.todoroo.astrid.core.SortHelper.SORT_LIST
import com.todoroo.astrid.core.SortHelper.SORT_START
import com.todoroo.astrid.ui.CheckableImageView
import org.tasks.R
@ -140,7 +141,7 @@ class TaskViewHolder internal constructor(
markdown.setMarkdown(nameView, task.title)
setupTitleAndCheckbox()
setupDueDate(sortMode == SORT_DUE)
setupChips(filter, sortMode == SORT_START)
setupChips(filter, sortMode == SORT_START, sortMode == SORT_LIST)
if (preferences.getBoolean(R.string.p_show_description, true)) {
markdown.setMarkdown(description, task.notes)
description.visibility = if (task.hasNotes()) View.VISIBLE else View.GONE
@ -215,7 +216,7 @@ class TaskViewHolder internal constructor(
}
}
private fun setupChips(filter: Filter, sortByStartDate: Boolean) {
private fun setupChips(filter: Filter, sortByStartDate: Boolean, sortByList: Boolean) {
chipGroup.setContent {
AppCompatTheme {
ChipGroup(
@ -238,6 +239,7 @@ class TaskViewHolder internal constructor(
isSubtask = task.hasParent(),
isGoogleTask = task.isGoogleTask,
sortByStartDate = sortByStartDate,
sortByList = sortByList,
toggleSubtasks = { task: Long, collapsed: Boolean -> callback.toggleSubtasks(task, collapsed) },
onClick = { it: Filter -> callback.onClick(it) },
)

@ -16,7 +16,7 @@ import org.tasks.preferences.ResourceResolver
import org.tasks.tasklist.TaskViewHolder.ViewHolderCallbacks
import org.tasks.ui.CheckBoxProvider
import org.tasks.ui.ChipProvider
import java.util.*
import java.util.Locale
import javax.inject.Inject
class ViewHolderFactory @Inject constructor(
@ -25,7 +25,9 @@ class ViewHolderFactory @Inject constructor(
private val chipProvider: ChipProvider,
private val checkBoxProvider: CheckBoxProvider,
private val linkify: Linkify,
private val locale: Locale) {
private val locale: Locale,
private val headerFormatter: HeaderFormatter,
) {
private val textColorSecondary: Int = ResourceResolver.getData(context, android.R.attr.textColorSecondary)
private val textColorOverdue: Int = context.getColor(R.color.overdue)
private val fontSize: Int = preferences.fontSize
@ -40,9 +42,10 @@ class ViewHolderFactory @Inject constructor(
fun newHeaderViewHolder(parent: ViewGroup?, callback: (Long) -> Unit) =
HeaderViewHolder(
context,
locale,
headerFormatter,
LayoutInflater.from(context).inflate(R.layout.task_adapter_header, parent, false),
callback)
callback,
)
fun newViewHolder(parent: ViewGroup?, callbacks: ViewHolderCallbacks) =
TaskViewHolder(

@ -97,6 +97,7 @@ class ChipProvider @Inject constructor(
place: Place?,
tagsString: String?,
sortByStartDate: Boolean,
sortByList: Boolean,
list: String?,
isSubtask: Boolean,
isGoogleTask: Boolean,
@ -125,7 +126,7 @@ class ChipProvider @Inject constructor(
)
}
if (!isSubtask && preferences.showListChip && filter !is CaldavFilter) {
if (!isSubtask && !sortByList && preferences.showListChip && filter !is CaldavFilter) {
remember(list, isGoogleTask) {
lists
.getCaldavList(list)

@ -26,13 +26,14 @@ import org.tasks.date.DateTimeUtils
import org.tasks.markdown.Markdown
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.tasklist.HeaderFormatter
import org.tasks.tasklist.SectionedDataSource
import org.tasks.tasklist.SectionedDataSource.Companion.HEADER_COMPLETED
import org.tasks.time.DateTimeUtils.startOfDay
import org.tasks.ui.CheckBoxProvider
import timber.log.Timber
import java.time.format.FormatStyle
import java.util.*
import java.util.Locale
import kotlin.math.max
internal class ScrollableViewsFactory(
@ -46,7 +47,8 @@ internal class ScrollableViewsFactory(
private val locale: Locale,
private val chipProvider: ChipProvider,
private val localBroadcastManager: LocalBroadcastManager,
private val markdown: Markdown
private val markdown: Markdown,
private val headerFormatter: HeaderFormatter,
) : RemoteViewsFactory {
private val indentPadding: Int
private var showDueDates = false
@ -145,13 +147,12 @@ internal class ScrollableViewsFactory(
val section = tasks.getSection(position)
val sortGroup = section.value
val header: String? = if (filter?.supportsSorting() == true) {
section.headerString(
context,
locale,
sortMode,
showFullDate,
FormatStyle.MEDIUM,
compact
headerFormatter.headerStringBlocking(
value = section.value,
sortMode = sortMode,
alwaysDisplayFullDate = showFullDate,
style = FormatStyle.MEDIUM,
compact = compact,
)
} else {
null

@ -13,6 +13,7 @@ import org.tasks.data.TaskDao;
import org.tasks.markdown.MarkdownProvider;
import org.tasks.preferences.DefaultFilterProvider;
import org.tasks.preferences.Preferences;
import org.tasks.tasklist.HeaderFormatter;
import org.tasks.themes.ColorProvider;
import org.tasks.ui.CheckBoxProvider;
@ -33,6 +34,7 @@ public class ScrollableWidgetUpdateService extends RemoteViewsService {
@Inject ChipProvider chipProvider;
@Inject LocalBroadcastManager localBroadcastManager;
@Inject MarkdownProvider markdownProvider;
@Inject HeaderFormatter headerFormatter;
@Override
public void onStart(Intent intent, int startId) {
@ -65,6 +67,8 @@ public class ScrollableWidgetUpdateService extends RemoteViewsService {
locale,
chipProvider,
localBroadcastManager,
markdownProvider.markdown(false));
markdownProvider.markdown(false),
headerFormatter
);
}
}

@ -42,6 +42,7 @@ File %1$s contained %2$s.\n\n
<string name="SSD_sort_importance">By priority</string>
<string name="SSD_sort_modified">By last modified</string>
<string name="sort_created">By creation time</string>
<string name="sort_list">By list</string>
<string name="FLA_search_filter">Matching \'%s\'</string>
<string name="FLA_new_filter">Create new filter</string>
<string name="TEA_title_hint">Task name</string>

Loading…
Cancel
Save