diff --git a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java index 94108ed32c..378f57f3c0 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/RecyclerAdapter.java @@ -21,7 +21,6 @@ package com.amaze.filemanager.adapters; import static com.amaze.filemanager.filesystem.compressed.CompressedHelper.*; -import static com.amaze.filemanager.filesystem.files.FileListSorter.SORT_NONE_ON_TOP; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_COLORIZE_ICONS; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_FILE_SIZE; import static com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_SHOW_GOBACK_BUTTON; @@ -52,6 +51,7 @@ import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.PasteHelper; import com.amaze.filemanager.filesystem.files.CryptUtil; +import com.amaze.filemanager.filesystem.files.sort.DirSortBy; import com.amaze.filemanager.ui.ItemPopupMenu; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.activities.superclasses.PreferenceActivity; @@ -626,7 +626,7 @@ private void setItems( public void createHeaders(boolean invalidate, List uris) { if ((mainFragment.getMainFragmentViewModel() != null - && mainFragment.getMainFragmentViewModel().getDsort() == SORT_NONE_ON_TOP) + && mainFragment.getMainFragmentViewModel().getDsort() == DirSortBy.NONE_ON_TOP) || getItemsDigested() == null || getItemsDigested().isEmpty()) { return; diff --git a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java index 0a4d34b670..6a2e574569 100644 --- a/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/adapters/data/LayoutElementParcelable.java @@ -25,6 +25,7 @@ import com.amaze.filemanager.fileoperations.filesystem.OpenMode; import com.amaze.filemanager.filesystem.HybridFileParcelable; +import com.amaze.filemanager.filesystem.files.sort.ComparableParcelable; import com.amaze.filemanager.ui.icons.Icons; import com.amaze.filemanager.utils.Utils; @@ -35,7 +36,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; -public class LayoutElementParcelable implements Parcelable { +public class LayoutElementParcelable implements Parcelable, ComparableParcelable { private static final String CURRENT_YEAR = String.valueOf(Calendar.getInstance().get(Calendar.YEAR)); @@ -275,4 +276,25 @@ public LayoutElementParcelable[] newArray(int size) { return new LayoutElementParcelable[size]; } }; + + @Override + public boolean isDirectory() { + return isDirectory; + } + + @NonNull + @Override + public String getParcelableName() { + return title; + } + + @Override + public long getDate() { + return date; + } + + @Override + public long getSize() { + return longSize; + } } diff --git a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java index fdf51c6b98..82c62951de 100644 --- a/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java +++ b/app/src/main/java/com/amaze/filemanager/asynchronous/asynctasks/LoadFilesListTask.java @@ -22,8 +22,6 @@ import static android.os.Build.VERSION.SDK_INT; import static android.os.Build.VERSION_CODES.Q; -import static com.amaze.filemanager.filesystem.files.FileListSorter.SORT_ASC; -import static com.amaze.filemanager.filesystem.files.FileListSorter.SORT_DSC; import java.io.File; import java.lang.ref.WeakReference; @@ -51,6 +49,7 @@ import com.amaze.filemanager.filesystem.SafRootHolder; import com.amaze.filemanager.filesystem.cloud.CloudUtil; import com.amaze.filemanager.filesystem.files.FileListSorter; +import com.amaze.filemanager.filesystem.files.sort.SortType; import com.amaze.filemanager.filesystem.root.ListFilesCommand; import com.amaze.filemanager.ui.activities.MainActivityViewModel; import com.amaze.filemanager.ui.fragments.CloudSheetFragment; @@ -254,17 +253,7 @@ private List getCachedMediaList( private void postListCustomPathProcess( @NonNull List list, @NonNull MainFragment mainFragment) { - int sortType = SortHandler.getSortType(context.get(), path); - int sortBy; - int isAscending; - - if (sortType <= 3) { - sortBy = sortType; - isAscending = SORT_ASC; - } else { - isAscending = SORT_DSC; - sortBy = sortType - 4; - } + SortType sortType = SortHandler.getSortType(context.get(), path); MainFragmentViewModel viewModel = mainFragment.getMainFragmentViewModel(); @@ -289,7 +278,7 @@ private void postListCustomPathProcess( } } - Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortBy, isAscending)); + Collections.sort(list, new FileListSorter(viewModel.getDsort(), sortType)); } private @Nullable LayoutElementParcelable createListParcelables(HybridFileParcelable baseFile) { diff --git a/app/src/main/java/com/amaze/filemanager/database/SortHandler.java b/app/src/main/java/com/amaze/filemanager/database/SortHandler.java index 6812dda727..2e8bedfc8a 100644 --- a/app/src/main/java/com/amaze/filemanager/database/SortHandler.java +++ b/app/src/main/java/com/amaze/filemanager/database/SortHandler.java @@ -30,6 +30,7 @@ import com.amaze.filemanager.application.AppConfig; import com.amaze.filemanager.database.models.explorer.Sort; +import com.amaze.filemanager.filesystem.files.sort.SortType; import android.content.Context; import android.content.SharedPreferences; @@ -60,23 +61,25 @@ public static SortHandler getInstance() { return SortHandlerHolder.INSTANCE; } - public static int getSortType(Context context, String path) { + public static SortType getSortType(Context context, String path) { SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context); final Set onlyThisFloders = sharedPref.getStringSet(PREFERENCE_SORTBY_ONLY_THIS, new HashSet<>()); final boolean onlyThis = onlyThisFloders.contains(path); final int globalSortby = Integer.parseInt(sharedPref.getString("sortby", "0")); + SortType globalSortType = SortType.getDirectorySortType(globalSortby); if (!onlyThis) { - return globalSortby; + return globalSortType; } Sort sort = SortHandler.getInstance().findEntry(path); if (sort == null) { - return globalSortby; + return globalSortType; } - return sort.type; + return SortType.getDirectorySortType(sort.type); } - public void addEntry(Sort sort) { + public void addEntry(String path, SortType sortType) { + Sort sort = new Sort(path, sortType.toDirectorySortInt()); database.sortDao().insert(sort).subscribeOn(Schedulers.io()).subscribe(); } @@ -84,7 +87,8 @@ public void clear(String path) { database.sortDao().clear(path).subscribeOn(Schedulers.io()).subscribe(); } - public void updateEntry(Sort oldSort, Sort newSort) { + public void updateEntry(Sort oldSort, String newPath, SortType newSortType) { + Sort newSort = new Sort(newPath, newSortType.toDirectorySortInt()); database.sortDao().update(newSort).subscribeOn(Schedulers.io()).subscribe(); } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java index 20edfc696e..09af943e9c 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java +++ b/app/src/main/java/com/amaze/filemanager/filesystem/HybridFileParcelable.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import com.amaze.filemanager.fileoperations.filesystem.OpenMode; +import com.amaze.filemanager.filesystem.files.sort.ComparableParcelable; import com.amaze.filemanager.filesystem.ftp.ExtensionsKt; import com.amaze.filemanager.utils.Utils; @@ -44,7 +45,7 @@ import net.schmizz.sshj.sftp.RemoteResourceInfo; import net.schmizz.sshj.xfer.FilePermission; -public class HybridFileParcelable extends HybridFile implements Parcelable { +public class HybridFileParcelable extends HybridFile implements Parcelable, ComparableParcelable { private final Logger LOG = LoggerFactory.getLogger(HybridFileParcelable.class); private long date, size; @@ -255,4 +256,10 @@ public int hashCode() { result = 37 * result + (int) (date ^ date >>> 32); return result; } + + @NonNull + @Override + public String getParcelableName() { + return getName(); + } } diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt index aac05a8f85..ba9c45ee92 100644 --- a/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/FileListSorter.kt @@ -20,31 +20,77 @@ package com.amaze.filemanager.filesystem.files -import androidx.annotation.IntDef import com.amaze.filemanager.adapters.data.LayoutElementParcelable +import com.amaze.filemanager.filesystem.files.sort.ComparableParcelable +import com.amaze.filemanager.filesystem.files.sort.DirSortBy +import com.amaze.filemanager.filesystem.files.sort.SortBy +import com.amaze.filemanager.filesystem.files.sort.SortType +import java.lang.Long import java.util.Locale /** * [Comparator] implementation to sort [LayoutElementParcelable]s. */ class FileListSorter( - @DirSortMode dirArg: Int, - @SortBy sortArg: Int, - @SortOrder ascArg: Int -) : Comparator { + dirArg: DirSortBy, + sortType: SortType, + searchTerm: String? +) : Comparator { private var dirsOnTop = dirArg - private val asc = ascArg - private val sort = sortArg + private val asc: Int = sortType.sortOrder.sortFactor + private val sort: SortBy = sortType.sortBy + + private val relevanceComparator: Comparator by lazy { + if (searchTerm == null) { + // no search term given, so every result is equally relevant + Comparator { _, _ -> + 0 + } + } else { + Comparator { o1, o2 -> + // Sorts in a way that least relevant is first + val comparator = compareBy { + // first we compare by the match percentage of the name + searchTerm.length.toDouble() / it.getParcelableName().length.toDouble() + }.thenBy { + // if match percentage is the same, we compare if the name starts with the match + it.getParcelableName().startsWith(searchTerm, ignoreCase = true) + }.thenBy { file -> + // if the match in the name could a word because it is surrounded by separators, it could be more relevant + // e.g. "my-cat" more relevant than "mysterious" + file.getParcelableName().split('-', '_', '.', ' ').any { + it.contentEquals( + searchTerm, + ignoreCase = true + ) + } + }.thenBy { file -> + // sort by modification date as last resort + file.getDate() + } + // Reverts the sorting to make most relevant first + comparator.compare(o1, o2) * -1 + } + } + } + + /** Constructor for convenience if there is no searchTerm */ + constructor(dirArg: DirSortBy, sortType: SortType) : this(dirArg, sortType, null) + + private fun isDirectory(path: ComparableParcelable): Boolean { + return path.isDirectory() + } - private fun isDirectory(path: LayoutElementParcelable): Boolean { - return path.isDirectory + /** Compares the names of [file1] and [file2] */ + private fun compareName(file1: ComparableParcelable, file2: ComparableParcelable): Int { + return file1.getParcelableName().compareTo(file2.getParcelableName(), ignoreCase = true) } /** * Compares two elements and return negative, zero and positive integer if first argument is less * than, equal to or greater than second */ - override fun compare(file1: LayoutElementParcelable, file2: LayoutElementParcelable): Int { + override fun compare(file1: ComparableParcelable, file2: ComparableParcelable): Int { /*File f1; if(!file1.hasSymlink()) { @@ -62,13 +108,13 @@ class FileListSorter( } else { f2=new File(file1.getSymlink()); }*/ - if (dirsOnTop == SORT_DIR_ON_TOP) { + if (dirsOnTop == DirSortBy.DIR_ON_TOP) { if (isDirectory(file1) && !isDirectory(file2)) { return -1 } else if (isDirectory(file2) && !isDirectory(file1)) { return 1 } - } else if (dirsOnTop == SORT_FILE_ON_TOP) { + } else if (dirsOnTop == DirSortBy.FILE_ON_TOP) { if (isDirectory(file1) && !isDirectory(file2)) { return 1 } else if (isDirectory(file2) && !isDirectory(file1)) { @@ -77,67 +123,46 @@ class FileListSorter( } when (sort) { - SORT_BY_NAME -> { + SortBy.NAME -> { // sort by name - return asc * file1.title.compareTo(file2.title, ignoreCase = true) + return asc * compareName(file1, file2) } - SORT_BY_LAST_MODIFIED -> { + SortBy.LAST_MODIFIED -> { // sort by last modified - return asc * java.lang.Long.valueOf(file1.date).compareTo(file2.date) + return asc * Long.valueOf(file1.getDate()).compareTo(file2.getDate()) } - SORT_BY_SIZE -> { + SortBy.SIZE -> { // sort by size - return if (!file1.isDirectory && !file2.isDirectory) { - asc * java.lang.Long.valueOf(file1.longSize).compareTo(file2.longSize) + return if (!isDirectory(file1) && !isDirectory(file2)) { + asc * Long.valueOf(file1.getSize()).compareTo(file2.getSize()) } else { - file1.title.compareTo(file2.title, ignoreCase = true) + compareName(file1, file2) } } - SORT_BY_TYPE -> { + SortBy.TYPE -> { // sort by type - return if (!file1.isDirectory && !file2.isDirectory) { - val ext_a = getExtension(file1.title) - val ext_b = getExtension(file2.title) + return if (!isDirectory(file1) && !isDirectory(file2)) { + val ext_a = getExtension(file1.getParcelableName()) + val ext_b = getExtension(file2.getParcelableName()) val res = asc * ext_a.compareTo(ext_b) if (res == 0) { - asc * file1.title.compareTo(file2.title, ignoreCase = true) + asc * compareName(file1, file2) } else { res } } else { - file1.title.compareTo(file2.title, ignoreCase = true) + compareName(file1, file2) } } - else -> return 0 + SortBy.RELEVANCE -> { + // sort by relevance to the search query + return asc * relevanceComparator.compare(file1, file2) + } } } companion object { - const val SORT_BY_NAME = 0 - const val SORT_BY_LAST_MODIFIED = 1 - const val SORT_BY_SIZE = 2 - const val SORT_BY_TYPE = 3 - - const val SORT_DIR_ON_TOP = 0 - const val SORT_FILE_ON_TOP = 1 - const val SORT_NONE_ON_TOP = 2 - - const val SORT_ASC = 1 - const val SORT_DSC = -1 - - @Retention(AnnotationRetention.SOURCE) - @IntDef(SORT_BY_NAME, SORT_BY_LAST_MODIFIED, SORT_BY_SIZE, SORT_BY_TYPE) - annotation class SortBy - - @Retention(AnnotationRetention.SOURCE) - @IntDef(SORT_DIR_ON_TOP, SORT_FILE_ON_TOP, SORT_NONE_ON_TOP) - annotation class DirSortMode - - @Retention(AnnotationRetention.SOURCE) - @IntDef(SORT_ASC, SORT_DSC) - annotation class SortOrder - /** * Convenience method to get the file extension in given path. * diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/ComparableParcelable.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/ComparableParcelable.kt new file mode 100644 index 0000000000..439e9eb349 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/ComparableParcelable.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem.files.sort + +/** Used by [FileListSorter] to get the needed information from a `Parcelable` */ +interface ComparableParcelable { + + /** Returns if the parcelable represents a directory */ + fun isDirectory(): Boolean + + /** Returns the name of the item represented by the parcelable */ + fun getParcelableName(): String + + /** Returns the date of the item represented by the parcelable as a Long */ + fun getDate(): Long + + /** Returns the size of the item represented by the parcelable */ + fun getSize(): Long +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/DirSortBy.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/DirSortBy.kt new file mode 100644 index 0000000000..05e96da51b --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/DirSortBy.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem.files.sort + +/** Represents the way in which directories and files should be sorted */ +enum class DirSortBy { + DIR_ON_TOP, + FILE_ON_TOP, + NONE_ON_TOP; + + companion object { + /** Returns the corresponding [DirSortBy] to [index] */ + @JvmStatic + fun getDirSortBy(index: Int): DirSortBy { + return when (index) { + 0 -> DIR_ON_TOP + 1 -> FILE_ON_TOP + 2 -> NONE_ON_TOP + else -> NONE_ON_TOP + } + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortBy.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortBy.kt new file mode 100644 index 0000000000..8d9d6b3f39 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortBy.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem.files.sort + +import android.content.Context +import com.amaze.filemanager.R + +/** + * Represents the sort by types. + * [index] is the index of the sort in the xml string array resource + * [sortDirectory] indicates if the sort can be used to sort an directory. + */ +enum class SortBy(val index: Int, val sortDirectory: Boolean) { + NAME(0, true), + LAST_MODIFIED(1, true), + SIZE(2, true), + TYPE(3, true), + RELEVANCE(4, false); + + /** Returns the corresponding string resource of the enum */ + fun toResourceString(context: Context): String { + return when (this) { + NAME -> context.resources.getString(R.string.sort_name) + LAST_MODIFIED -> context.resources.getString(R.string.lastModified) + SIZE -> context.resources.getString(R.string.sort_size) + TYPE -> context.resources.getString(R.string.type) + RELEVANCE -> context.resources.getString(R.string.sort_relevance) + } + } + + companion object { + const val NAME_INDEX = 0 + const val LAST_MODIFIED_INDEX = 1 + const val SIZE_INDEX = 2 + const val TYPE_INDEX = 3 + const val RELEVANCE_INDEX = 4 + + /** Returns the SortBy corresponding to [index] which can be used to sort directories */ + @JvmStatic + fun getDirectorySortBy(index: Int): SortBy { + return when (index) { + NAME_INDEX -> NAME + LAST_MODIFIED_INDEX -> LAST_MODIFIED + SIZE_INDEX -> SIZE + TYPE_INDEX -> TYPE + else -> NAME + } + } + + /** Returns the SortBy corresponding to [index] */ + @JvmStatic + fun getSortBy(index: Int): SortBy { + return when (index) { + NAME_INDEX -> NAME + LAST_MODIFIED_INDEX -> LAST_MODIFIED + SIZE_INDEX -> SIZE + TYPE_INDEX -> TYPE + RELEVANCE_INDEX -> RELEVANCE + else -> NAME + } + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortOrder.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortOrder.kt new file mode 100644 index 0000000000..d60d8504fd --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortOrder.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem.files.sort + +/** + * Represents the direction the sort should be ordered + * + * [sortFactor] is the factor that should be multiplied to the result of `compareTo()` to achieve the correct sort direction + */ +enum class SortOrder(val sortFactor: Int) { + ASC(1), + DESC(-1) +} diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortType.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortType.kt new file mode 100644 index 0000000000..c3042cfda0 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/sort/SortType.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014-2023 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.amaze.filemanager.filesystem.files.sort + +/** Describes how to sort with [sortBy] and which direction to use for the sort with [sortOrder] */ +data class SortType(val sortBy: SortBy, val sortOrder: SortOrder) { + + /** + * Returns the Int corresponding to the combination of [sortBy] and [sortOrder] + */ + fun toDirectorySortInt(): Int { + val sortIndex = if (sortBy.sortDirectory) sortBy.index else 0 + return when (sortOrder) { + SortOrder.ASC -> sortIndex + SortOrder.DESC -> sortIndex + 4 + } + } + + companion object { + /** + * Returns the [SortType] with the [SortBy] and [SortOrder] corresponding to [index] + */ + @JvmStatic + fun getDirectorySortType(index: Int): SortType { + val sortOrder = if (index <= 3) SortOrder.ASC else SortOrder.DESC + val normalizedIndex = if (index <= 3) index else index - 4 + val sortBy = SortBy.getDirectorySortBy(normalizedIndex) + return SortType(sortBy, sortOrder) + } + } +} diff --git a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java index c6ad3aa548..5bf8700de2 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java +++ b/app/src/main/java/com/amaze/filemanager/ui/dialogs/GeneralDialogCreation.java @@ -60,6 +60,9 @@ import com.amaze.filemanager.filesystem.compressed.CompressedHelper; import com.amaze.filemanager.filesystem.files.EncryptDecryptUtils; import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.sort.SortBy; +import com.amaze.filemanager.filesystem.files.sort.SortOrder; +import com.amaze.filemanager.filesystem.files.sort.SortType; import com.amaze.filemanager.filesystem.root.ChangeFilePermissionsCommand; import com.amaze.filemanager.ui.ExtensionsKt; import com.amaze.filemanager.ui.activities.MainActivity; @@ -953,12 +956,12 @@ public static void showSortDialog( final String path = m.getCurrentPath(); int accentColor = m.getMainActivity().getAccent(); String[] sort = m.getResources().getStringArray(R.array.sortby); - int current = SortHandler.getSortType(m.getContext(), path); + SortType current = SortHandler.getSortType(m.getContext(), path); MaterialDialog.Builder a = new MaterialDialog.Builder(m.getActivity()); a.theme(appTheme.getMaterialDialogTheme(m.requireContext())); a.items(sort) .itemsCallbackSingleChoice( - current > 3 ? current - 4 : current, (dialog, view, which, text) -> true); + current.getSortBy().getIndex(), (dialog, view, which, text) -> true); final Set sortbyOnlyThis = sharedPref.getStringSet(PREFERENCE_SORTBY_ONLY_THIS, Collections.emptySet()); final Set onlyThisFloders = new HashSet<>(sortbyOnlyThis); @@ -981,11 +984,11 @@ public static void showSortDialog( a.positiveText(R.string.descending).negativeColor(accentColor); a.onNegative( (dialog, which) -> { - onSortTypeSelected(m, sharedPref, onlyThisFloders, dialog, false); + onSortTypeSelected(m, sharedPref, onlyThisFloders, dialog, SortOrder.ASC); }); a.onPositive( (dialog, which) -> { - onSortTypeSelected(m, sharedPref, onlyThisFloders, dialog, true); + onSortTypeSelected(m, sharedPref, onlyThisFloders, dialog, SortOrder.DESC); }); a.title(R.string.sort_by); a.build().show(); @@ -996,20 +999,20 @@ private static void onSortTypeSelected( SharedPreferences sharedPref, Set onlyThisFloders, MaterialDialog dialog, - boolean desc) { - final int sortType = desc ? dialog.getSelectedIndex() + 4 : dialog.getSelectedIndex(); + SortOrder sortOrder) { + final SortType sortType = + new SortType(SortBy.getDirectorySortBy(dialog.getSelectedIndex()), sortOrder); SortHandler sortHandler = SortHandler.getInstance(); if (onlyThisFloders.contains(m.getCurrentPath())) { Sort oldSort = sortHandler.findEntry(m.getCurrentPath()); - Sort newSort = new Sort(m.getCurrentPath(), sortType); if (oldSort == null) { - sortHandler.addEntry(newSort); + sortHandler.addEntry(m.getCurrentPath(), sortType); } else { - sortHandler.updateEntry(oldSort, newSort); + sortHandler.updateEntry(oldSort, m.getCurrentPath(), sortType); } } else { sortHandler.clear(m.getCurrentPath()); - sharedPref.edit().putString("sortby", String.valueOf(sortType)).apply(); + sharedPref.edit().putString("sortby", String.valueOf(sortType.toDirectorySortInt())).apply(); } sharedPref.edit().putStringSet(PREFERENCE_SORTBY_ONLY_THIS, onlyThisFloders).apply(); m.updateList(false); diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java index ba6eb7c918..3a4222e399 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/MainFragment.java @@ -1564,9 +1564,7 @@ public void onSearchCompleted(final String query) { new SortSearchResultTask( elements, new FileListSorter( - mainFragmentViewModel.getDsort(), - mainFragmentViewModel.getSortby(), - mainFragmentViewModel.getAsc()), + mainFragmentViewModel.getDsort(), mainFragmentViewModel.getSortType()), this, query)); } diff --git a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt index 0dd638e396..71d80bafff 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt +++ b/app/src/main/java/com/amaze/filemanager/ui/fragments/data/MainFragmentViewModel.kt @@ -31,9 +31,10 @@ import com.amaze.filemanager.adapters.data.LayoutElementParcelable import com.amaze.filemanager.database.CloudHandler import com.amaze.filemanager.fileoperations.filesystem.OpenMode import com.amaze.filemanager.filesystem.HybridFileParcelable -import com.amaze.filemanager.filesystem.files.FileListSorter.Companion.DirSortMode -import com.amaze.filemanager.filesystem.files.FileListSorter.Companion.SortBy -import com.amaze.filemanager.filesystem.files.FileListSorter.Companion.SortOrder +import com.amaze.filemanager.filesystem.files.sort.DirSortBy +import com.amaze.filemanager.filesystem.files.sort.SortBy +import com.amaze.filemanager.filesystem.files.sort.SortOrder +import com.amaze.filemanager.filesystem.files.sort.SortType import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_GRID_COLUMNS import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants.PREFERENCE_GRID_COLUMNS_DEFAULT @@ -59,14 +60,9 @@ class MainFragmentViewModel : ViewModel() { var searchHelper = ArrayList() var no = 0 - @SortBy - var sortby = 0 + var sortType: SortType = SortType(SortBy.NAME, SortOrder.ASC) - @DirSortMode - var dsort = 0 - - @SortOrder - var asc = 0 + var dsort: DirSortBy = DirSortBy.DIR_ON_TOP var home: String? = null @@ -174,19 +170,13 @@ class MainFragmentViewModel : ViewModel() { * * Final value of [.sortby] varies from 0 to 3 */ - fun initSortModes(sortType: Int, sharedPref: SharedPreferences) { - if (sortType <= 3) { - sortby = sortType - asc = 1 - } else { - asc = -1 - sortby = sortType - 4 - } + fun initSortModes(sortType: SortType, sharedPref: SharedPreferences) { + this.sortType = sortType sharedPref.getString( PreferencesConstants.PREFERENCE_DIRECTORY_SORT_MODE, "0" )?.run { - dsort = Integer.parseInt(this) + dsort = DirSortBy.getDirSortBy(Integer.parseInt(this)) } } diff --git a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java index 78fedb8851..743e3b9be8 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java +++ b/app/src/main/java/com/amaze/filemanager/ui/views/appbar/SearchView.java @@ -24,9 +24,18 @@ import static android.os.Build.VERSION.SDK_INT; import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import com.afollestad.materialdialogs.MaterialDialog; import com.amaze.filemanager.R; import com.amaze.filemanager.adapters.SearchRecyclerViewAdapter; +import com.amaze.filemanager.filesystem.HybridFileParcelable; +import com.amaze.filemanager.filesystem.files.FileListSorter; +import com.amaze.filemanager.filesystem.files.sort.DirSortBy; +import com.amaze.filemanager.filesystem.files.sort.SortBy; +import com.amaze.filemanager.filesystem.files.sort.SortOrder; +import com.amaze.filemanager.filesystem.files.sort.SortType; import com.amaze.filemanager.ui.activities.MainActivity; import com.amaze.filemanager.ui.fragments.preferencefragments.PreferencesConstants; import com.amaze.filemanager.ui.theme.AppTheme; @@ -41,7 +50,9 @@ import android.annotation.SuppressLint; import android.content.Context; import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.Spannable; import android.text.SpannableString; @@ -55,10 +66,12 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; +import androidx.appcompat.widget.AppCompatButton; import androidx.appcompat.widget.AppCompatEditText; import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; +import androidx.core.content.res.ResourcesCompat; import androidx.core.widget.NestedScrollView; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.RecyclerView; @@ -88,6 +101,18 @@ public class SearchView { private final SearchRecyclerViewAdapter searchRecyclerViewAdapter; + /** Text to describe {@link SearchView#searchResultsSortButton} */ + private final AppCompatTextView searchResultsSortHintTV; + + /** The button to select how the results should be sorted */ + private final AppCompatButton searchResultsSortButton; + + /** The drawable used to indicate that the search results are sorted ascending */ + private final Drawable searchResultsSortAscDrawable; + + /** The drawable used to indicate that the search results are sorted descending */ + private final Drawable searchResultsSortDescDrawable; + // 0 -> Basic Search // 1 -> Indexed Search // 2 -> Deep Search @@ -95,6 +120,11 @@ public class SearchView { private boolean enabled = false; + private final SortType defaultSortType = new SortType(SortBy.RELEVANCE, SortOrder.ASC); + + /** The selected sort type for the search results */ + private SortType sortType = defaultSortType; + @SuppressWarnings("ConstantConditions") @SuppressLint("NotifyDataSetChanged") public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchListener) { @@ -111,6 +141,20 @@ public SearchView(final AppBar appbar, MainActivity mainActivity, SearchListener searchResultsHintTV = mainActivity.findViewById(R.id.searchResultsHintTV); deepSearchTV = mainActivity.findViewById(R.id.searchDeepSearchTV); recyclerView = mainActivity.findViewById(R.id.searchRecyclerView); + searchResultsSortHintTV = mainActivity.findViewById(R.id.searchResultsSortHintTV); + searchResultsSortButton = mainActivity.findViewById(R.id.searchResultsSortButton); + searchResultsSortAscDrawable = + ResourcesCompat.getDrawable( + mainActivity.getResources(), + R.drawable.baseline_sort_24_asc_white, + mainActivity.getTheme()); + searchResultsSortDescDrawable = + ResourcesCompat.getDrawable( + mainActivity.getResources(), + R.drawable.baseline_sort_24_desc_white, + mainActivity.getTheme()); + + setUpSearchResultsSortButton(); initRecentSearches(mainActivity); @@ -156,7 +200,7 @@ public void afterTextChanged(Editable s) {} deepSearchTV.setOnClickListener( v -> { - String s = searchViewEditText.getText().toString().trim(); + String s = getSearchTerm(); if (searchMode == 1) { @@ -168,10 +212,7 @@ public void afterTextChanged(Editable s) {} .indexedSearch(mainActivity, s) .observe( mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), - hybridFileParcelables -> { - searchRecyclerViewAdapter.submitList(hybridFileParcelables); - searchRecyclerViewAdapter.notifyDataSetChanged(); - }); + hybridFileParcelables -> updateResultList(hybridFileParcelables, s)); searchMode = 2; @@ -195,7 +236,7 @@ public void afterTextChanged(Editable s) {} @SuppressWarnings("ConstantConditions") private boolean onSearch(boolean shouldSave) { - String s = searchViewEditText.getText().toString().trim(); + String s = getSearchTerm(); if (s.isEmpty()) { searchViewEditText.setError(mainActivity.getString(R.string.field_empty)); @@ -215,6 +256,8 @@ private void basicSearch(String s) { clearRecyclerView(); searchResultsHintTV.setVisibility(View.VISIBLE); + searchResultsSortButton.setVisibility(View.VISIBLE); + searchResultsSortHintTV.setVisibility(View.VISIBLE); deepSearchTV.setVisibility(View.VISIBLE); searchMode = 1; deepSearchTV.setText( @@ -228,10 +271,7 @@ private void basicSearch(String s) { .basicSearch(mainActivity, s) .observe( mainActivity.getCurrentMainFragment().getViewLifecycleOwner(), - hybridFileParcelables -> { - searchRecyclerViewAdapter.submitList(hybridFileParcelables); - searchRecyclerViewAdapter.notifyItemInserted(hybridFileParcelables.size() + 1); - }); + hybridFileParcelables -> updateResultList(hybridFileParcelables, s)); } private void saveRecentPreference(String s) { @@ -309,12 +349,27 @@ private void resetSearchMode() { deepSearchTV.setVisibility(View.GONE); } + /** + * Updates the list of results displayed in {@link SearchView#searchRecyclerViewAdapter} sorted + * according to the current {@link SearchView#sortType} + * + * @param newResults The list of results that should be displayed + * @param searchTerm The search term that resulted in the search results + */ + private void updateResultList(List newResults, String searchTerm) { + ArrayList items = new ArrayList<>(newResults); + Collections.sort(items, new FileListSorter(DirSortBy.NONE_ON_TOP, sortType, searchTerm)); + searchRecyclerViewAdapter.submitList(items); + searchRecyclerViewAdapter.notifyDataSetChanged(); + } + /** show search view with a circular reveal animation */ public void revealSearchView() { final int START_RADIUS = 16; int endRadius = Math.max(appbar.getToolbar().getWidth(), appbar.getToolbar().getHeight()); resetSearchMode(); + resetSearchResultsSortButton(); clearRecyclerView(); Animator animator; @@ -366,6 +421,79 @@ public void onAnimationRepeat(Animator animation) {} }); } + /** + * Sets up the {@link SearchView#searchResultsSortButton} to show a dialog when it is clicked. The + * text and icon of {@link SearchView#searchResultsSortButton} is also set to the current {@link + * SearchView#sortType} + */ + private void setUpSearchResultsSortButton() { + searchResultsSortButton.setOnClickListener(v -> showSearchResultsSortDialog()); + updateSearchResultsSortButtonDisplay(); + } + + /** Builds and shows a dialog for selection which sort should be applied for the search results */ + private void showSearchResultsSortDialog() { + int accentColor = mainActivity.getAccent(); + new MaterialDialog.Builder(mainActivity) + .items(R.array.sortbySearch) + .itemsCallbackSingleChoice( + sortType.getSortBy().getIndex(), (dialog, itemView, which, text) -> true) + .negativeText(R.string.ascending) + .positiveColor(accentColor) + .onNegative( + (dialog, which) -> onSortTypeSelected(dialog, dialog.getSelectedIndex(), SortOrder.ASC)) + .positiveText(R.string.descending) + .negativeColor(accentColor) + .onPositive( + (dialog, which) -> + onSortTypeSelected(dialog, dialog.getSelectedIndex(), SortOrder.DESC)) + .title(R.string.sort_by) + .build() + .show(); + } + + private void onSortTypeSelected(MaterialDialog dialog, int index, SortOrder sortOrder) { + this.sortType = new SortType(SortBy.getSortBy(index), sortOrder); + dialog.dismiss(); + updateSearchResultsSortButtonDisplay(); + updateResultList(searchRecyclerViewAdapter.getCurrentList(), getSearchTerm()); + } + + private void resetSearchResultsSortButton() { + sortType = defaultSortType; + updateSearchResultsSortButtonDisplay(); + } + + /** Updates the text and icon of {@link SearchView#searchResultsSortButton} */ + private void updateSearchResultsSortButtonDisplay() { + searchResultsSortButton.setText(sortType.getSortBy().toResourceString(mainActivity)); + setSearchResultSortOrderIcon(); + } + + /** + * Updates the icon of {@link SearchView#searchResultsSortButton} and colors it to fit the text + * color + */ + private void setSearchResultSortOrderIcon() { + Drawable orderDrawable; + switch (sortType.getSortOrder()) { + default: + case ASC: + orderDrawable = searchResultsSortAscDrawable; + break; + case DESC: + orderDrawable = searchResultsSortDescDrawable; + break; + } + + orderDrawable.setColorFilter( + new PorterDuffColorFilter( + mainActivity.getResources().getColor(R.color.accent_material_light), + PorterDuff.Mode.SRC_ATOP)); + searchResultsSortButton.setCompoundDrawablesWithIntrinsicBounds( + null, null, orderDrawable, null); + } + /** hide search view with a circular reveal animation */ public void hideSearchView() { final int END_RADIUS = 16; @@ -464,6 +592,8 @@ private void clearRecyclerView() { searchRecyclerViewAdapter.notifyDataSetChanged(); searchResultsHintTV.setVisibility(View.GONE); + searchResultsSortHintTV.setVisibility(View.GONE); + searchResultsSortButton.setVisibility(View.GONE); } private SpannableString getSpannableText(String s1, String s2) { @@ -484,6 +614,15 @@ private SpannableString getSpannableText(String s1, String s2) { return spannableString; } + /** + * Returns the current text in {@link SearchView#searchViewEditText} + * + * @return The current search text + */ + private String getSearchTerm() { + return searchViewEditText.getText().toString().trim(); + } + public interface SearchListener { void onSearch(String queue); } diff --git a/app/src/main/res/drawable/baseline_sort_24_asc_white.xml b/app/src/main/res/drawable/baseline_sort_24_asc_white.xml new file mode 100644 index 0000000000..218906d965 --- /dev/null +++ b/app/src/main/res/drawable/baseline_sort_24_asc_white.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_sort_24_desc_white.xml b/app/src/main/res/drawable/baseline_sort_24_desc_white.xml new file mode 100644 index 0000000000..d79a05bc39 --- /dev/null +++ b/app/src/main/res/drawable/baseline_sort_24_desc_white.xml @@ -0,0 +1,6 @@ + + + diff --git a/app/src/main/res/layout-v21/layout_search.xml b/app/src/main/res/layout-v21/layout_search.xml index 4d292b2695..17f265bb15 100644 --- a/app/src/main/res/layout-v21/layout_search.xml +++ b/app/src/main/res/layout-v21/layout_search.xml @@ -24,11 +24,11 @@ android:layout_marginLeft="@dimen/search_view_back_margin_left_right" android:layout_marginRight="@dimen/search_view_back_margin_left_right" android:background="@drawable/ripple" - app:srcCompat="@drawable/ic_arrow_back_black_24dp" app:layout_constraintBottom_toBottomOf="@id/search_edit_text" app:layout_constraintEnd_toStartOf="@id/search_edit_text" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/search_edit_text" /> + app:layout_constraintTop_toTopOf="@id/search_edit_text" + app:srcCompat="@drawable/ic_arrow_back_black_24dp" /> + app:layout_constraintTop_toTopOf="@id/search_edit_text" + app:srcCompat="@drawable/ic_close_black_24dp" /> + + +