diff --git a/app/schemas/com.hippo.ehviewer.dao.EhDatabase/19.json b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/19.json new file mode 100644 index 000000000..66a6484f0 --- /dev/null +++ b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/19.json @@ -0,0 +1,465 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "edd2f8e628072889f7702b50eb2c44a1", + "entities": [ + { + "tableName": "GALLERIES", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT NOT NULL, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "TOKEN", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "TITLE", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "titleJpn", + "columnName": "TITLE_JPN", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbKey", + "columnName": "THUMB", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "CATEGORY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "posted", + "columnName": "POSTED", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "UPLOADER", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rating", + "columnName": "RATING", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "simpleLanguage", + "columnName": "SIMPLE_LANGUAGE", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favoriteSlot", + "columnName": "FAVORITE_SLOT", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DOWNLOAD_LABELS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "label", + "columnName": "LABEL", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "POSITION", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_DOWNLOAD_LABELS_LABEL", + "unique": true, + "columnNames": [ + "LABEL" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "DOWNLOADS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "STATE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "legacy", + "columnName": "LEGACY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "TIME", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "LABEL", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [ + { + "name": "index_DOWNLOADS_LABEL", + "unique": false, + "columnNames": [ + "LABEL" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)" + } + ], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + }, + { + "table": "DOWNLOAD_LABELS", + "onDelete": "SET NULL", + "onUpdate": "CASCADE", + "columns": [ + "LABEL" + ], + "referencedColumns": [ + "LABEL" + ] + } + ] + }, + { + "tableName": "DOWNLOAD_DIRNAME", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dirname", + "columnName": "DIRNAME", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FILTER", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "mode", + "columnName": "MODE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "TEXT", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "ENABLE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "HISTORY", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "TIME", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + } + ] + }, + { + "tableName": "LOCAL_FAVORITES", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "TIME", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + } + ] + }, + { + "tableName": "PROGRESS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "page", + "columnName": "PAGE", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + } + ] + }, + { + "tableName": "QUICK_SEARCH", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "NAME", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "MODE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "CATEGORY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "KEYWORD", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "advanceSearch", + "columnName": "ADVANCE_SEARCH", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minRating", + "columnName": "MIN_RATING", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageFrom", + "columnName": "PAGE_FROM", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageTo", + "columnName": "PAGE_TO", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "POSITION", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'edd2f8e628072889f7702b50eb2c44a1')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt index 28875b706..9112e4c2f 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt @@ -57,7 +57,7 @@ object EhDB { } } - suspend fun updateDownloadInfo(downloadInfo: List) { + suspend fun updateDownloadInfo(downloadInfo: Collection) { val dao = db.downloadsDao() dao.update(downloadInfo.map(DownloadInfo::downloadInfo)) } @@ -70,7 +70,6 @@ object EhDB { suspend fun removeDownloadInfo(downloadInfo: DownloadInfo) { val dao = db.downloadsDao() dao.delete(downloadInfo.downloadInfo) - dao.fill(downloadInfo.position) deleteGalleryInfo(downloadInfo.galleryInfo) } @@ -82,7 +81,6 @@ object EhDB { dao.delete(it.downloadInfo) deleteGalleryInfo(it.galleryInfo) } - dao.fill(downloadInfo.first().position, downloadInfo.size) } suspend fun getDownloadDirname(gid: Long): String? { @@ -106,6 +104,9 @@ object EhDB { dao.insertOrIgnore(downloadDirnameList) } + val downloadsCount + get() = db.downloadsDao().count() + suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list() suspend fun addDownloadLabel(raw: DownloadLabel): DownloadLabel { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt index a1e6e15f4..83ea613e0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.remember import com.hippo.ehviewer.client.CHROME_USER_AGENT import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.data.FavListUrlBuilder +import com.hippo.ehviewer.download.SortMode import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import java.util.Locale @@ -108,8 +109,11 @@ object Settings : DataStorePreferences(null) { val thumbColumns = intPref("thumb_columns", 3) val listThumbSize = intPref("list_tile_size", 40) val languageFilter = intPref("language_filter", -1) + val downloadSortMode = intPref("download_sort_mode", SortMode.Default.flag) val meteredNetworkWarning = boolPref("cellular_network_warning", false) val blackDarkTheme = boolPref("black_dark_theme", false) + val gridView = boolPref("grid_view", false) + val recentDownloadLabel = stringOrNullPref("recent_download_label", null) var downloadScheme by stringOrNullPref("image_scheme", null) var downloadAuthority by stringOrNullPref("image_authority", null) @@ -150,7 +154,6 @@ object Settings : DataStorePreferences(null) { var downloadOriginImage by boolPref("download_origin_image", false) var archiveMetadata by boolPref("archive_metadata", true) var enableCronet by boolPref("enable_cronet", true) - var gridView by boolPref("grid_view", false) var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL) var defaultFavSlot by intPref("default_favorite_slot", -2) var securityDelay by intPref("require_unlock_delay", 0) @@ -161,7 +164,6 @@ object Settings : DataStorePreferences(null) { var lastDawnDays by intPref("last_dawn_days", 0) var recentToplist by stringPref("recent_toplist", "11") var userAgent by stringPref("user_agent", CHROME_USER_AGENT) - var recentDownloadLabel by stringOrNullPref("recent_download_label", null) var defaultDownloadLabel by stringOrNullPref("default_download_label", null) var displayName by stringOrNullPref("display_name", null) var avatar by stringOrNullPref("avatar", null) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/AbstractDownloadInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/AbstractDownloadInfo.kt index a1aa6a44a..7fe9c6a8c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/AbstractDownloadInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/AbstractDownloadInfo.kt @@ -5,7 +5,6 @@ interface AbstractDownloadInfo { var legacy: Int var time: Long var label: String? - var position: Int var speed: Long var remaining: Long var finished: Int diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt index c31ac39e6..80d9e1bcd 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt @@ -15,6 +15,7 @@ */ package com.hippo.ehviewer.client.data +import com.hippo.ehviewer.util.containsIgnoreCase import java.util.regex.Pattern import moe.tarsin.kt.unreachable @@ -146,3 +147,6 @@ fun GalleryInfo.findBaseInfo(): BaseGalleryInfo { fun GalleryInfo.asGalleryDetail(): GalleryDetail? { return this as? GalleryDetail } + +operator fun GalleryInfo.contains(keyword: String): Boolean = + arrayOf(title, titleJpn, uploader).any { it.containsIgnoreCase(keyword) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt index 16106c05b..fa2b64b9b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt @@ -107,12 +107,15 @@ class ThumbKeyMigration : AutoMigrationSpec { } } +@RenameColumn(tableName = "DOWNLOADS", fromColumnName = "POSITION", toColumnName = "TIME") +class DownloadsMigration : AutoMigrationSpec + @Database( entities = [ BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class, ], - version = 18, + version = 19, exportSchema = true, autoMigrations = [ AutoMigration( @@ -178,6 +181,11 @@ class ThumbKeyMigration : AutoMigrationSpec { from = 17, to = 18, ), + AutoMigration( + from = 18, + to = 19, + spec = DownloadsMigration::class, + ), ], ) @TypeConverters(FilterModeConverter::class) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt index 25a2d52d4..217c68720 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt @@ -42,9 +42,6 @@ data class DownloadEntity( @ColumnInfo(name = "LABEL", index = true) override var label: String? = null, - @ColumnInfo(name = "POSITION") - override var position: Int = 0, - @Ignore override var speed: Long = 0, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadLabelDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadLabelDao.kt index 21a7901c1..e545bdb5a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadLabelDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadLabelDao.kt @@ -8,10 +8,10 @@ import androidx.room.Update @Dao interface DownloadLabelDao { - @Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION ASC") + @Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION") suspend fun list(): List - @Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION ASC LIMIT :limit OFFSET :offset") + @Query("SELECT * FROM DOWNLOAD_LABELS ORDER BY POSITION LIMIT :limit OFFSET :offset") suspend fun list(offset: Int, limit: Int): List @Query("UPDATE DOWNLOAD_LABELS SET POSITION = POSITION - 1 WHERE POSITION > :position") diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt index 6c5366bed..c1a39b2ae 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt @@ -3,23 +3,26 @@ package com.hippo.ehviewer.dao import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow @Dao interface DownloadsDao { - @Query("SELECT * FROM DOWNLOADS ORDER BY POSITION DESC") + @Suppress("ktlint:standard:annotation") + @Query("SELECT LABEL, COUNT(*) AS COUNT FROM DOWNLOADS LEFT JOIN DOWNLOAD_LABELS USING(LABEL) GROUP BY LABEL") + fun count(): Flow> + + @Query("SELECT * FROM DOWNLOADS ORDER BY TIME") suspend fun list(): List @Transaction - @Query("SELECT * FROM DOWNLOADS LEFT JOIN DOWNLOAD_DIRNAME USING(GID) ORDER BY POSITION DESC") + @Query("SELECT * FROM DOWNLOADS LEFT JOIN DOWNLOAD_DIRNAME USING(GID) ORDER BY TIME DESC") suspend fun joinList(): List - @Query("UPDATE DOWNLOADS SET POSITION = POSITION - :offset WHERE POSITION > :position") - suspend fun fill(position: Int, offset: Int = 1) - @Update suspend fun update(downloadInfo: List) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt index c705f26c9..bcee98b5c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt @@ -11,7 +11,7 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo @Dao interface HistoryDao { - @Query("SELECT * FROM HISTORY ORDER BY TIME DESC") + @Query("SELECT * FROM HISTORY ORDER BY TIME") suspend fun list(): List @RewriteQueriesToDropUnusedColumns diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt index aef1fe8f1..38c7c3ddd 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt @@ -15,7 +15,7 @@ interface LocalFavoritesDao { @Query("SELECT COUNT(*) FROM LOCAL_FAVORITES") fun count(): Flow - @Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME DESC") + @Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME") suspend fun list(): List @RewriteQueriesToDropUnusedColumns @@ -27,7 +27,7 @@ interface LocalFavoritesDao { fun joinListLazy(title: String): PagingSource @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY RANDOM() DESC LIMIT 1") + @Query("SELECT * FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) ORDER BY RANDOM() LIMIT 1") suspend fun random(): BaseGalleryInfo @Query("SELECT EXISTS(SELECT * FROM LOCAL_FAVORITES WHERE GID = :gid)") diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/QuickSearchDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/QuickSearchDao.kt index a70659d4d..4ecb4ba0a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/QuickSearchDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/QuickSearchDao.kt @@ -8,10 +8,10 @@ import androidx.room.Update @Dao interface QuickSearchDao { - @Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION ASC") + @Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION") suspend fun list(): List - @Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION ASC LIMIT :limit OFFSET :offset") + @Query("SELECT * FROM QUICK_SEARCH ORDER BY POSITION LIMIT :limit OFFSET :offset") suspend fun list(offset: Int, limit: Int): List @Query("UPDATE QUICK_SEARCH SET POSITION = POSITION - 1 WHERE POSITION > :position") diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt index bbe695380..1dab36d14 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -22,9 +22,7 @@ import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.toMutableStateList import com.google.android.material.math.MathUtils import com.hippo.ehviewer.EhDB @@ -39,7 +37,7 @@ import com.hippo.ehviewer.spider.putToDownloadDir import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.ConcurrentPool import com.hippo.ehviewer.util.SimpleHandler -import com.hippo.ehviewer.util.assertNotMainThread +import com.hippo.ehviewer.util.insertWith import com.hippo.ehviewer.util.mapNotNull import com.hippo.ehviewer.util.runAssertingNotMainThread import com.hippo.unifile.UniFile @@ -57,20 +55,19 @@ import splitties.preferences.edit object DownloadManager : OnSpiderListener { // All download info list - val allInfoList = runAssertingNotMainThread { EhDB.getAllDownloadInfo() }.toMutableStateList() + private val allInfoList = runAssertingNotMainThread { + (EhDB.getAllDownloadInfo() as MutableList).apply { sortWith(comparator()) } + } + + val downloadInfoList: List + get() = allInfoList // All download info map private val mAllInfoMap = allInfoList.associateBy { it.gid } as MutableMap - // label and info list map, without default label info list - private val map: MutableMap> - // All labels without default label val labelList = runAssertingNotMainThread { EhDB.getAllDownloadLabelList() }.toMutableStateList() - // Store download info with default label - private val defaultInfoList: SnapshotStateList - // Store download info wait to start private val mWaitList = ArrayDeque() private val mSpeedReminder = SpeedReminder() @@ -84,31 +81,6 @@ object DownloadManager : OnSpiderListener { private val mutex = Mutex() - init { - assertNotMainThread() - map = runAssertingNotMainThread { - allInfoList.groupBy { it.label }.entries.associate { - it.key?.let { label -> - if (!containLabel(label)) { - // Add non existing label to DB and list - labelList.add(EhDB.addDownloadLabel(DownloadLabel(label, labelList.size))) - } - } - it.key to it.value.toMutableStateList() - } - } as MutableMap> - defaultInfoList = map.remove(null) ?: mutableStateListOf() - labelList.forEach { (label) -> - map[label] ?: mutableStateListOf().also { map[label] = it } - } - } - - fun getInfoListForLabel(label: String?) = if (label == null) { - defaultInfoList - } else { - map[label] - } - fun containLabel(label: String?): Boolean { if (label == null) { return false @@ -195,18 +167,9 @@ object DownloadManager : OnSpiderListener { info = DownloadInfo(galleryInfo, galleryInfo.putToDownloadDir()) info.label = label info.state = DownloadInfo.STATE_WAIT - info.position = allInfoList.size - - // Add to label download list - val list = getInfoListForLabel(info.label) - if (list == null) { - logcat(TAG, LogPriority.ERROR) { "Can't find download info list with label: $label" } - return - } - list.add(0, info) // Add to all download list and map - allInfoList.add(0, info) + allInfoList.insertWith(info, comparator()) mAllInfoMap[galleryInfo.gid] = info // Add to wait list @@ -281,51 +244,40 @@ object DownloadManager : OnSpiderListener { } suspend fun addDownload(downloadInfoList: List) { - downloadInfoList.filterNot { containDownloadInfo(it.gid) }.forEach { info -> + val comparator = comparator() + downloadInfoList.forEach { info -> + if (containDownloadInfo(info.gid)) return@forEach + // Ensure download state if (DownloadInfo.STATE_WAIT == info.state || DownloadInfo.STATE_DOWNLOAD == info.state) { info.state = DownloadInfo.STATE_NONE } - info.position = allInfoList.size - - // Add to label download list - val list = getInfoListForLabel(info.label) ?: addLabel(info.label).let { getInfoListForLabel(info.label)!! } - list.add(info) - // Sort - list.sortByPositionDescending() // Add to all download list and map - allInfoList.add(info) + allInfoList.insertWith(info, comparator) mAllInfoMap[info.gid] = info // Save to EhDB.putDownloadInfo(info) } - - // Sort all download list - allInfoList.sortByPositionDescending() } suspend fun addDownloadLabel(downloadLabelList: List) { val offset = downloadLabelList.size - downloadLabelList.filterNot { containLabel(it.label) }.forEachIndexed { index, label -> - map[label.label] = mutableStateListOf() - label.position = index + offset - labelList.add(EhDB.addDownloadLabel(label)) + downloadLabelList.forEachIndexed { index, label -> + if (!containLabel(label.label)) { + label.position = index + offset + labelList.add(EhDB.addDownloadLabel(label)) + } } } suspend fun restoreDownload(galleryInfo: BaseGalleryInfo, dirname: String) { val info = DownloadInfo(galleryInfo, dirname) info.state = DownloadInfo.STATE_NONE - info.position = allInfoList.size - - // Add to default label download list - val list = defaultInfoList - list.add(0, info) // Add to all download list and map - allInfoList.add(0, info) + allInfoList.insertWith(info, comparator()) mAllInfoMap[galleryInfo.gid] = info // Save to @@ -397,16 +349,8 @@ object DownloadManager : OnSpiderListener { allInfoList.remove(info) mAllInfoMap.remove(info.gid) - // Remove label list - val list = getInfoListForLabel(info.label) - if (list != null) { - val index = list.indexOf(info) - if (index >= 0) { - list.remove(info) - // Update listener - mutableNotifyFlow.emit(info) - } - } + // Update listener + mutableNotifyFlow.emit(info) // Ensure download ensureDownload() @@ -421,7 +365,7 @@ object DownloadManager : OnSpiderListener { suspend fun deleteRangeDownload(gidList: LongArray) { stopRangeDownloadInternal(gidList) val list = gidList.mapNotNull { gid -> - mAllInfoMap.remove(gid)?.also { getInfoListForLabel(it.label)?.remove(it) } + mAllInfoMap.remove(gid) } EhDB.removeDownloadInfo(list) allInfoList.removeAll(list.toSet()) @@ -433,25 +377,8 @@ object DownloadManager : OnSpiderListener { ensureDownload() } - private fun moveDownload(fromPosition: Int, toPosition: Int): List { - val info = allInfoList.removeAt(fromPosition) - allInfoList.add(toPosition, info) - val range = if (fromPosition < toPosition) fromPosition..toPosition else toPosition..fromPosition - val list = allInfoList.slice(range) - val limit = allInfoList.size - 1 - list.zip(range).forEach { it.first.position = limit - it.second } - getInfoListForLabel(info.label)!!.sortByPositionDescending() - return list - } - - fun moveDownload(fromItem: DownloadInfo, toItem: DownloadInfo): List { - val fromPosition = allInfoList.indexOf(fromItem) - val toPosition = allInfoList.indexOf(toItem) - return if (fromPosition != -1 && toPosition != -1) { - moveDownload(fromPosition, toPosition) - } else { - emptyList() - } + fun sortDownloads(mode: SortMode) { + allInfoList.sortWith(mode.comparator()) } suspend fun resetAllReadingProgress() = runCatching { @@ -551,28 +478,10 @@ object DownloadManager : OnSpiderListener { logcat(TAG, LogPriority.ERROR) { "Not exits label: $label" } return } - val dstList = getInfoListForLabel(label) - if (dstList == null) { - logcat(TAG, LogPriority.ERROR) { "Can't find label with label: $label" } - return - } - for (info in list) { - if (info.label == label) { - continue - } - val srcList = getInfoListForLabel(info.label) - if (srcList == null) { - logcat(TAG, LogPriority.ERROR) { "Can't find label with label: " + info.label } - continue - } - srcList.remove(info) - dstList.add(info) - info.label = label - dstList.sortByPositionDescending() - - // Save to DB - EhDB.putDownloadInfo(info) + list.forEach { + it.label = label } + EhDB.updateDownloadInfo(list) } suspend fun addLabel(label: String?) { @@ -580,7 +489,6 @@ object DownloadManager : OnSpiderListener { return } labelList.add(EhDB.addDownloadLabel(DownloadLabel(label, labelList.size))) - map[label] = mutableStateListOf() } suspend fun renameLabel(from: String, to: String) { @@ -590,37 +498,27 @@ object DownloadManager : OnSpiderListener { val new = exist.copy(label = to) labelList.add(index, new) EhDB.updateDownloadLabel(new) - val list = map.remove(from)?.onEach { it.label = to } - if (list != null) map[to] = list + allInfoList.forEach { + if (it.label == from) { + it.label = to + } + } } } suspend fun deleteLabel(label: String) { - // Find in label list and remove - var found = false - val iterator = labelList.iterator() - while (iterator.hasNext()) { - val raw = iterator.next() - if (label == raw.label) { - found = true - iterator.remove() - EhDB.removeDownloadLabel(raw) - break + labelList.run { + val index = indexOfFirst { it.label == label } + subList(index + 1, size).forEach { + it.position-- + } + EhDB.removeDownloadLabel(removeAt(index)) + } + allInfoList.forEach { + if (it.label == label) { + it.label = null } } - if (!found) { - return - } - val list = map.remove(label) ?: return - - // Update info label - for (info in list) { - info.label = null - defaultInfoList.add(info) - } - - // Sort - defaultInfoList.sortByPositionDescending() } val isIdle: Boolean @@ -995,9 +893,7 @@ object DownloadManager : OnSpiderListener { private const val TYPE_ON_PAGE_FAILURE = 4 private const val TYPE_ON_FINISH = 5 - private fun MutableList.sortByPositionDescending() { - sortByDescending { it.position } - } + private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator() } var downloadLocation: UniFile diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/SortMode.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/SortMode.kt new file mode 100644 index 000000000..97af6bd3d --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/SortMode.kt @@ -0,0 +1,51 @@ +package com.hippo.ehviewer.download + +import com.hippo.ehviewer.client.EhUtils +import com.hippo.ehviewer.dao.DownloadInfo + +data class SortMode(val field: Field, val order: Order) { + val flag + get() = field.value shl 1 or order.value + + enum class Field(val value: Int) { + GID(0), + TITLE(1), + TIME(2), + LABEL(3), + } + + enum class Order(val value: Int) { + ASC(0), + DESC(1), + } + + fun comparator(): Comparator { + val comparator = when (field) { + Field.GID -> compareBy { it.gid } + Field.TITLE -> compareBy { EhUtils.getSuitableTitle(it) } + Field.TIME -> compareBy { it.time } + Field.LABEL -> compareBy { it.label }.thenBy { EhUtils.getSuitableTitle(it) } + } + return if (order == Order.ASC) comparator else comparator.reversed() + } + + companion object { + fun from(flag: Int): SortMode { + val field = flag shr 1 + val order = flag and 1 + require(field in Field.entries.indices && order in Order.entries.indices) + return SortMode(Field.entries[field], Order.entries[order]) + } + val Default = SortMode(Field.TIME, Order.DESC) + val All = listOf( + SortMode(Field.TIME, Order.DESC), + SortMode(Field.TIME, Order.ASC), + SortMode(Field.GID, Order.DESC), + SortMode(Field.GID, Order.ASC), + SortMode(Field.TITLE, Order.ASC), + SortMode(Field.TITLE, Order.DESC), + SortMode(Field.LABEL, Order.ASC), + SortMode(Field.LABEL, Order.DESC), + ) + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt index 0e3bf6c03..e9dfbc693 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/DownloadCard.kt @@ -37,23 +37,19 @@ import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.ui.tools.CrystalCard -import com.hippo.ehviewer.ui.tools.DragHandle import com.hippo.ehviewer.ui.tools.GalleryListCardRating import com.hippo.ehviewer.util.FileUtils -import sh.calvin.reorderable.ReorderableItemScope @Composable -fun ReorderableItemScope.DownloadCard( +fun DownloadCard( onClick: () -> Unit, onThumbClick: () -> Unit, onLongClick: () -> Unit, onStart: () -> Unit, onStop: () -> Unit, - onDragStarted: () -> Unit, - onDragStopped: () -> Unit, info: DownloadInfo, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { CrystalCard( modifier = modifier, @@ -169,7 +165,6 @@ fun ReorderableItemScope.DownloadCard( bottom.linkTo(parent.bottom) }, ) { - DragHandle(onDragStarted = onDragStarted, onDragStopped = onDragStopped) if (downloadState == DownloadInfo.STATE_WAIT || downloadState == DownloadInfo.STATE_DOWNLOAD) { IconButton(onClick = onStop) { Icon(imageVector = Icons.Default.Pause, contentDescription = null) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt index 20edc364b..fd7628507 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/main/FabLayout.kt @@ -106,7 +106,6 @@ fun FabLayout( expanded: Boolean, onExpandChanged: (Boolean) -> Unit, autoCancel: Boolean, - rotateWhenExpand: Boolean = true, fabBuilder: FabBuilder.() -> Unit, ) { val updatedHidden by rememberUpdatedState(hidden) @@ -174,12 +173,8 @@ fun FabLayout( Icon( imageVector = Icons.Default.Close, contentDescription = null, - modifier = if (rotateWhenExpand) { - Modifier.graphicsLayer { - rotationZ = lerp(-135f, 0f, animatedProgress) - } - } else { - Modifier + modifier = Modifier.graphicsLayer { + rotationZ = lerp(-135f, 0f, animatedProgress) }, ) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt index 89bddb92e..2aabb99f0 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt @@ -1,6 +1,7 @@ package com.hippo.ehviewer.ui.screen import android.content.Intent +import android.view.ViewConfiguration import androidx.compose.animation.Crossfade import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.clickable @@ -19,6 +20,7 @@ import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells @@ -26,16 +28,19 @@ import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.DriveFileMove +import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.automirrored.filled.ViewList import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.DoneAll import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.FilterList import androidx.compose.material.icons.filled.GridView import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.NewLabel import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Shuffle import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -47,6 +52,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateListOf @@ -76,11 +82,14 @@ import arrow.core.partially1 import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings +import com.hippo.ehviewer.asMutableState +import com.hippo.ehviewer.client.data.contains import com.hippo.ehviewer.collectAsState import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager.labelList import com.hippo.ehviewer.download.DownloadService +import com.hippo.ehviewer.download.SortMode import com.hippo.ehviewer.icons.EhIcons import com.hippo.ehviewer.icons.big.Download import com.hippo.ehviewer.ui.LocalSideSheetState @@ -88,6 +97,7 @@ import com.hippo.ehviewer.ui.LockDrawer import com.hippo.ehviewer.ui.MainActivity import com.hippo.ehviewer.ui.confirmRemoveDownloadRange import com.hippo.ehviewer.ui.main.DownloadCard +import com.hippo.ehviewer.ui.main.FAB_ANIMATE_TIME import com.hippo.ehviewer.ui.main.FabLayout import com.hippo.ehviewer.ui.main.GalleryInfoGridItem import com.hippo.ehviewer.ui.navToReader @@ -98,9 +108,9 @@ import com.hippo.ehviewer.ui.tools.FastScrollLazyColumn import com.hippo.ehviewer.ui.tools.FastScrollLazyVerticalStaggeredGrid import com.hippo.ehviewer.ui.tools.LocalDialogState import com.hippo.ehviewer.ui.tools.SwipeToDismissBox2 +import com.hippo.ehviewer.ui.tools.delegateSnapshotUpdate import com.hippo.ehviewer.ui.tools.draggingHapticFeedback -import com.hippo.ehviewer.ui.tools.observed -import com.hippo.ehviewer.util.containsIgnoreCase +import com.hippo.ehviewer.ui.tools.rememberInVM import com.hippo.ehviewer.util.findActivity import com.hippo.ehviewer.util.mapToLongArray import com.ramcosta.composedestinations.annotation.Destination @@ -108,22 +118,26 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import kotlin.math.roundToInt -import kotlin.math.sign import kotlinx.coroutines.CancellationException import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import moe.tarsin.coroutines.onEachLatest import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyColumnState @Destination @Composable fun DownloadsScreen(navigator: DestinationsNavigator) { - var label by Settings::recentDownloadLabel.observed - var gridView by Settings::gridView.observed + var label by Settings.recentDownloadLabel.asMutableState() + var gridView by Settings.gridView.asMutableState() + var sortMode by Settings.downloadSortMode.asMutableState() var keyword by rememberSaveable { mutableStateOf(null) } var filterType by rememberSaveable { mutableIntStateOf(-1) } var searchBarOffsetY by remember(label) { mutableIntStateOf(0) } val searchFieldState = rememberTextFieldState() + + var fabExpanded by remember { mutableStateOf(false) } + var fabHidden by remember { mutableStateOf(false) } val checkedInfoMap = remember { mutableStateMapOf() } val selectMode by rememberUpdatedState(checkedInfoMap.isNotEmpty()) LockDrawer(selectMode) @@ -138,12 +152,12 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { val defaultName = stringResource(R.string.default_download_label_name) val title = stringResource(R.string.scene_download_title, if (label == "") allName else label ?: defaultName) val hint = stringResource(R.string.search_bar_hint, title) - val list = remember(label, filterType, keyword) { - val raw = DownloadManager.getInfoListForLabel(label) ?: DownloadManager.allInfoList - raw.filterTo(mutableStateListOf()) { info -> - (filterType == -1 || info.state == filterType) && keyword?.let { - info.title.containsIgnoreCase(it) || info.titleJpn.containsIgnoreCase(it) || info.uploader.containsIgnoreCase(it) - } ?: true + val sortModes = stringArrayResource(id = R.array.download_sort_modes) + val downloadStates = stringArrayResource(id = R.array.download_state) + val list = remember(label, filterType, keyword, sortMode) { + DownloadManager.downloadInfoList.filterTo(mutableStateListOf()) { info -> + (label == "" || info.label == label) && (filterType == -1 || info.state == filterType) && + keyword.orEmpty() in info } } @@ -153,6 +167,8 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { val labelEmpty = stringResource(R.string.label_text_is_empty) val defaultInvalid = stringResource(R.string.label_text_is_invalid) val labelExists = stringResource(R.string.label_text_exist) + val downloadsCount by rememberInVM { EhDB.downloadsCount }.collectAsState(emptyMap()) + val totalCount = remember(downloadsCount) { downloadsCount.values.sum() } with(activity) { ProvideSideSheetContent { drawerState -> fun closeSheet() = coroutineScope.launch { drawerState.close() } @@ -218,7 +234,6 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(), ) { stickyHeader { - val all = DownloadManager.allInfoList ListItem( modifier = Modifier.clickable { label = "" @@ -227,12 +242,11 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { tonalElevation = 1.dp, shadowElevation = 1.dp, headlineContent = { - Text("$allName [${all.size}]") + Text("$allName [$totalCount]") }, ) } stickyHeader { - val default = DownloadManager.getInfoListForLabel(null)!! ListItem( modifier = Modifier.clickable { label = null @@ -241,7 +255,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { tonalElevation = 1.dp, shadowElevation = 1.dp, headlineContent = { - Text("$defaultName [${default.size}]") + Text("$defaultName [${downloadsCount.getOrDefault(null, 0)}]") }, ) } @@ -251,7 +265,9 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { if (it == SwipeToDismissBoxValue.EndToStart) { coroutineScope.launch { DownloadManager.deleteLabel(item) - label = "" + if (label == item) { + label = "" + } } } true @@ -270,8 +286,6 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { }, label = "elevation", ) - val thatList = DownloadManager.getInfoListForLabel(item) - val text = if (thatList != null) "$item [${thatList.size}]" else item ListItem( modifier = Modifier.clickable { label = item @@ -280,7 +294,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { tonalElevation = 1.dp, shadowElevation = elevation, headlineContent = { - Text(text) + Text("$item [${downloadsCount.getOrDefault(item, 0)}]") }, trailingContent = { Row { @@ -334,10 +348,12 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { title = title, searchFieldState = searchFieldState, searchFieldHint = hint, - showSearchFab = selectMode, onApplySearch = { keyword = it.takeUnless { it.isBlank() } }, - onSearchExpanded = { checkedInfoMap.clear() }, - onSearchHidden = {}, + onSearchExpanded = { + checkedInfoMap.clear() + fabHidden = true + }, + onSearchHidden = { fabHidden = false }, searchBarOffsetY = { searchBarOffsetY }, trailingIcon = { var expanded by remember { mutableStateOf(false) } @@ -349,7 +365,6 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { IconButton(onClick = { expanded = !expanded }) { Icon(imageVector = Icons.Default.MoreVert, contentDescription = null) } - val states = stringArrayResource(id = R.array.download_state) DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { DropdownMenuItem( text = { Text(text = stringResource(id = R.string.download_labels)) }, @@ -360,15 +375,6 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { } }, ) - DropdownMenuItem( - text = { Text(text = stringResource(id = R.string.download_filter)) }, - onClick = { - expanded = false - coroutineScope.launch { - filterType = dialogState.showSingleChoice(states.toList(), filterType + 1) - 1 - } - }, - ) DropdownMenuItem( text = { Text(text = stringResource(id = R.string.download_start_all)) }, onClick = { @@ -429,10 +435,17 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { end = contentPadding.calculateEndPadding(layoutDirection) + marginH, ) val searchBarConnection = remember { + val slop = ViewConfiguration.get(context).scaledTouchSlop val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() } object : NestedScrollConnection { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - searchBarOffsetY = (searchBarOffsetY + consumed.y).roundToInt().coerceIn(-topPaddingPx, 0) + val dy = -consumed.y + if (dy >= slop) { + fabHidden = true + } else if (dy <= -slop / 2) { + fabHidden = false + } + searchBarOffsetY = (searchBarOffsetY - dy).roundToInt().coerceIn(-topPaddingPx, 0) return Offset.Zero // We never consume it } } @@ -459,80 +472,52 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { onClick = ::onItemClick.partially1(it), onLongClick = { navigator.navigate(it.galleryInfo.asDst()) }, info = it, + modifier = Modifier.animateItemPlacement(), ) } } } else { - val listState = rememberLazyListState() - val reorderableState = rememberReorderableLazyColumnState(listState) { from, to -> - val fromIndex = from.index - 1 - val toIndex = to.index - 1 - list.apply { add(toIndex, removeAt(fromIndex)) } - view.performHapticFeedback(draggingHapticFeedback) - } - var fromIndex by remember { mutableIntStateOf(-1) } FastScrollLazyColumn( modifier = Modifier.nestedScroll(searchBarConnection).fillMaxSize(), - state = listState, contentPadding = realPadding, verticalArrangement = Arrangement.spacedBy(dimensionResource(R.dimen.gallery_list_interval)), ) { - // Fix the first item's reorder animation - item {} - itemsIndexed(list, key = { _, item -> item.gid }) { index, info -> - ReorderableItem(reorderableState, key = info.gid) { - val checked = info.gid in checkedInfoMap - CheckableItem(checked = checked) { interactionSource -> - DownloadCard( - onClick = { - if (selectMode) { - if (checked) { - checkedInfoMap.remove(info.gid) - } else { - checkedInfoMap[info.gid] = info - } + items(list, key = { it.gid }) { info -> + val checked = info.gid in checkedInfoMap + CheckableItem(checked = checked, modifier = Modifier.animateItemPlacement()) { interactionSource -> + DownloadCard( + onClick = { + if (selectMode) { + if (checked) { + checkedInfoMap.remove(info.gid) } else { - onItemClick(info) + checkedInfoMap[info.gid] = info } - }, - onThumbClick = { - navigator.navigate(info.galleryInfo.asDst()) - }, - onLongClick = { - checkedInfoMap[info.gid] = info - }, - onStart = { - val intent = Intent(activity, DownloadService::class.java) - intent.action = DownloadService.ACTION_START - intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo) - ContextCompat.startForegroundService(activity, intent) - }, - onStop = { - coroutineScope.launchIO { - DownloadManager.stopDownload(info.gid) - } - }, - onDragStarted = { - fromIndex = index - }, - onDragStopped = { - if (fromIndex != -1) { - val direction = (index - fromIndex).sign - if (direction != 0) { - val toItem = list[index - direction] - coroutineScope.launchIO { - val toUpdate = DownloadManager.moveDownload(info, toItem) - EhDB.updateDownloadInfo(toUpdate) - } - } - fromIndex = -1 - } - }, - info = info, - interactionSource = interactionSource, - modifier = Modifier.height(height), - ) - } + } else { + onItemClick(info) + } + }, + onThumbClick = { + navigator.navigate(info.galleryInfo.asDst()) + }, + onLongClick = { + checkedInfoMap[info.gid] = info + }, + onStart = { + val intent = Intent(activity, DownloadService::class.java) + intent.action = DownloadService.ACTION_START + intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo) + ContextCompat.startForegroundService(activity, intent) + }, + onStop = { + coroutineScope.launchIO { + DownloadManager.stopDownload(info.gid) + } + }, + info = info, + modifier = Modifier.height(height), + interactionSource = interactionSource, + ) } } } @@ -561,40 +546,77 @@ fun DownloadsScreen(navigator: DestinationsNavigator) { } } + val hideFab by delegateSnapshotUpdate { + record { fabHidden } + transform { + // Bug: IDE failed to inference 'hide's type + onEachLatest { hide: Boolean -> + if (!hide) delay(FAB_ANIMATE_TIME.toLong()) + } + } + } + FabLayout( - hidden = !selectMode, - expanded = selectMode, - onExpandChanged = { checkedInfoMap.clear() }, - autoCancel = false, - rotateWhenExpand = false, + hidden = hideFab && !selectMode, + expanded = fabExpanded || selectMode, + onExpandChanged = { + fabExpanded = it + checkedInfoMap.clear() + }, + autoCancel = !selectMode, ) { - onClick(Icons.Default.DoneAll) { - val info = list.associateBy { it.gid } - checkedInfoMap.putAll(info) - throw CancellationException() - } - onClick(Icons.Default.PlayArrow) { - val gidList = checkedInfoMap.run { toMap().values.also { clear() } }.mapToLongArray(DownloadInfo::gid) - val intent = Intent(activity, DownloadService::class.java) - intent.action = DownloadService.ACTION_START_RANGE - intent.putExtra(DownloadService.KEY_GID_LIST, gidList) - ContextCompat.startForegroundService(context, intent) - } - onClick(Icons.Default.Pause) { - val gidList = checkedInfoMap.run { toMap().values.also { clear() } }.mapToLongArray(DownloadInfo::gid) - DownloadManager.stopRangeDownload(gidList) - } - onClick(Icons.Default.Delete) { - val infoList = checkedInfoMap.run { toMap().values.also { clear() } } - dialogState.confirmRemoveDownloadRange(infoList) - list.removeAll(infoList) - } - onClick(Icons.AutoMirrored.Default.DriveFileMove) { - val infoList = checkedInfoMap.run { toMap().values.also { clear() } } - val toLabel = dialogState.showMoveDownloadLabelList(infoList) - if (label != "" && label != toLabel) { + if (!selectMode) { + onClick(Icons.Default.Shuffle) { + context.navToReader(list.random().galleryInfo) + } + onClick(Icons.AutoMirrored.Default.Sort) { + val selected = dialogState.showSingleChoice( + sortModes.toList(), + SortMode.All.indexOfFirst { it.flag == sortMode }, + R.string.sort_by, + ) + val mode = SortMode.All[selected] + DownloadManager.sortDownloads(mode) + sortMode = mode.flag + } + onClick(Icons.Default.FilterList) { + filterType = dialogState.showSingleChoice( + downloadStates.toList(), + filterType + 1, + R.string.download_filter, + ) - 1 + } + } else { + onClick(Icons.Default.DoneAll) { + val info = list.associateBy { it.gid } + checkedInfoMap.putAll(info) + throw CancellationException() + } + onClick(Icons.Default.PlayArrow) { + val gidList = checkedInfoMap.run { toMap().values.also { clear() } } + .mapToLongArray(DownloadInfo::gid) + val intent = Intent(activity, DownloadService::class.java) + intent.action = DownloadService.ACTION_START_RANGE + intent.putExtra(DownloadService.KEY_GID_LIST, gidList) + ContextCompat.startForegroundService(context, intent) + } + onClick(Icons.Default.Pause) { + val gidList = checkedInfoMap.run { toMap().values.also { clear() } } + .mapToLongArray(DownloadInfo::gid) + DownloadManager.stopRangeDownload(gidList) + } + onClick(Icons.Default.Delete) { + val infoList = checkedInfoMap.run { toMap().values.also { clear() } } + dialogState.confirmRemoveDownloadRange(infoList) list.removeAll(infoList) } + onClick(Icons.AutoMirrored.Default.DriveFileMove) { + val infoList = checkedInfoMap.run { toMap().values.also { clear() } } + val toLabel = dialogState.showMoveDownloadLabelList(infoList) + if (label != "" && label != toLabel) { + list.removeAll(infoList) + } + } } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt index d94bd9c38..73e47e0e4 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/FavoritesScreen.kt @@ -1,5 +1,6 @@ package com.hippo.ehviewer.ui.screen +import android.view.ViewConfiguration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets @@ -226,8 +227,8 @@ fun FavouritesScreen(navigator: DestinationsNavigator) { data.loadState.refresh is LoadState.NotLoading } - var expanded by remember { mutableStateOf(false) } - var hidden by remember { mutableStateOf(false) } + var fabExpanded by remember { mutableStateOf(false) } + var fabHidden by remember { mutableStateOf(false) } val checkedInfoMap = remember { mutableStateMapOf() } val selectMode = checkedInfoMap.isNotEmpty() LockDrawer(selectMode) @@ -239,9 +240,9 @@ fun FavouritesScreen(navigator: DestinationsNavigator) { onApplySearch = { refresh(FavListUrlBuilder(urlBuilder.favCat, it)) }, onSearchExpanded = { checkedInfoMap.clear() - hidden = true + fabHidden = true }, - onSearchHidden = { hidden = false }, + onSearchHidden = { fabHidden = false }, refreshState = refreshState, searchBarOffsetY = { searchBarOffsetY }, trailingIcon = { @@ -255,10 +256,17 @@ fun FavouritesScreen(navigator: DestinationsNavigator) { val height by collectListThumbSizeAsState() val showPages = Settings.showGalleryPages val searchBarConnection = remember { + val slop = ViewConfiguration.get(context).scaledTouchSlop val topPaddingPx = with(density) { contentPadding.calculateTopPadding().roundToPx() } object : NestedScrollConnection { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { - searchBarOffsetY = (searchBarOffsetY + consumed.y).roundToInt().coerceIn(-topPaddingPx, 0) + val dy = -consumed.y + if (dy >= slop) { + fabHidden = true + } else if (dy <= -slop / 2) { + fabHidden = false + } + searchBarOffsetY = (searchBarOffsetY - dy).roundToInt().coerceIn(-topPaddingPx, 0) return Offset.Zero // We never consume it } } @@ -270,7 +278,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) { listMode = listMode, detailItemContent = { info -> val checked = info.gid in checkedInfoMap - CheckableItem(checked = checked) { interactionSource -> + CheckableItem(checked = checked, modifier = Modifier.animateItemPlacement()) { interactionSource -> GalleryInfoListItem( onClick = { if (selectMode) { @@ -296,7 +304,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) { }, thumbItemContent = { info -> val checked = info.gid in checkedInfoMap - CheckableItem(checked = checked) { interactionSource -> + CheckableItem(checked = checked, modifier = Modifier.animateItemPlacement()) { interactionSource -> GalleryInfoGridItem( onClick = { if (selectMode) { @@ -325,7 +333,7 @@ fun FavouritesScreen(navigator: DestinationsNavigator) { } val hideFab by delegateSnapshotUpdate { - record { hidden } + record { fabHidden } transform { // Bug: IDE failed to inference 'hide's type onEachLatest { hide: Boolean -> @@ -335,10 +343,10 @@ fun FavouritesScreen(navigator: DestinationsNavigator) { } FabLayout( - hidden = hideFab, - expanded = expanded || selectMode, + hidden = hideFab && !selectMode, + expanded = fabExpanded || selectMode, onExpandChanged = { - expanded = it + fabExpanded = it checkedInfoMap.clear() }, autoCancel = !selectMode, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryHolder.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryHolder.kt index a33e5131c..851edbfd6 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryHolder.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryHolder.kt @@ -25,9 +25,13 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @Composable -fun CheckableItem(checked: Boolean, content: @Composable (MutableInteractionSource) -> Unit) { +fun CheckableItem( + checked: Boolean, + modifier: Modifier = Modifier, + content: @Composable (MutableInteractionSource) -> Unit, +) { val src = controlledInteractionSource(enabled = checked) - Box { + Box(modifier) { content(src) if (checked) { Icon( diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt index f25a64e70..de720148e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt @@ -204,6 +204,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { } } + // Fix crash when TextField with focus is scaled to 0 val focusManager = LocalFocusManager.current LaunchedEffect(showSearchLayout) { if (!showSearchLayout) { @@ -421,9 +422,15 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { itemsIndexed(quickSearchList, key = { _, item -> item.id!! }) { index, item -> ReorderableItem(reorderableLazyListState, key = item.id!!) { isDragging -> val dismissState = rememberSwipeToDismissBoxState( - confirmValueChange = { - if (it == SwipeToDismissBoxValue.EndToStart) { - quickSearchList.remove(item) + confirmValueChange = { value -> + if (value == SwipeToDismissBoxValue.EndToStart) { + quickSearchList.run { + val removeIndex = indexOf(item) + subList(removeIndex + 1, size).forEach { + it.position-- + } + removeAt(removeIndex) + } coroutineScope.launchIO { EhDB.deleteQuickSearch(item) } @@ -502,7 +509,8 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { data.loadState.refresh is LoadState.NotLoading } - var hidden by remember { mutableStateOf(false) } + var fabExpanded by remember { mutableStateOf(false) } + var fabHidden by remember { mutableStateOf(false) } val openGalleryKeyword = stringResource(R.string.gallery_list_search_bar_open_gallery) abstract class UrlSuggestion : Suggestion() { @@ -523,8 +531,6 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { override val destination = ProgressScreenDestination(gid, pToken, page) } - var expanded by remember { mutableStateOf(false) } - val selectImageFirst = stringResource(R.string.select_image_first) SearchBarScreen( title = suitableTitle, @@ -571,8 +577,8 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { } showSearchLayout = false }, - onSearchExpanded = { hidden = true }, - onSearchHidden = { hidden = false }, + onSearchExpanded = { fabHidden = true }, + onSearchHidden = { fabHidden = false }, refreshState = refreshState, suggestionProvider = { GalleryDetailUrlParser.parse(it, false)?.run { @@ -667,9 +673,9 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { val dy = -consumed.y if (dy >= slop) { - hidden = true + fabHidden = true } else if (dy <= -slop / 2) { - hidden = false + fabHidden = false } searchBarOffsetY = (searchBarOffsetY - dy).roundToInt().coerceIn(-topPaddingPx, 0) return Offset.Zero // We never consume it @@ -731,7 +737,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { val outOfRange = stringResource(R.string.error_out_of_range) val hideFab by delegateSnapshotUpdate { - record { hidden || showSearchLayout } + record { fabHidden || showSearchLayout } transform { // Bug: IDE failed to inference 'hide's type onEachLatest { hide: Boolean -> @@ -742,8 +748,8 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) { FabLayout( hidden = hideFab, - expanded = expanded, - onExpandChanged = { expanded = it }, + expanded = fabExpanded, + onExpandChanged = { fabExpanded = it }, autoCancel = true, ) { if (urlBuilder.mode in arrayOf(MODE_NORMAL, MODE_UPLOADER, MODE_TAG)) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt index 7045457cd..17da0759e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/Dialog.kt @@ -408,8 +408,16 @@ class DialogState { suspend fun showSingleChoice( items: List, selected: Int, + @StringRes title: Int? = null, ): Int = showNoButton { Column(modifier = Modifier.verticalScroll(rememberScrollState()).padding(vertical = 8.dp)) { + title?.let { + Text( + text = stringResource(id = it), + modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), + style = MaterialTheme.typography.headlineSmall, + ) + } items.forEachIndexed { index, text -> Row( modifier = Modifier.clickable { dismissWith(index) }.fillMaxWidth() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/util/CollectionsExtensions.kt b/app/src/main/kotlin/com/hippo/ehviewer/util/CollectionsExtensions.kt new file mode 100644 index 000000000..14ba5f196 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/util/CollectionsExtensions.kt @@ -0,0 +1,8 @@ +package com.hippo.ehviewer.util + +fun MutableList.insertWith(element: T, comparator: Comparator) { + val index = binarySearch(element, comparator).let { + if (it < 0) -it - 1 else it + } + add(index, element) +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2d15fc28a..64fd9ab70 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -456,6 +456,15 @@ 纯黑深色主题 类别颜色适配动态配色 使用持续集成(CI)构建频道更新 + 排序依据 + 添加时间(降序) + 添加时间(升序) + 上传时间(降序) + 上传时间(升序) + 标题(升序) + 标题(降序) + 下载标签(升序) + 下载标签(降序) 过滤 全部 全部开始(倒序) diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index e66589da2..5548b07c1 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -228,6 +228,17 @@ 2 + + @string/added_time_desc + @string/added_time_asc + @string/uploaded_time_desc + @string/uploaded_time_asc + @string/title_asc + @string/title_desc + @string/download_label_asc + @string/download_label_desc + + @string/download_all @string/download_state_none diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fb4c5c576..78da1c822 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -521,6 +521,15 @@ Dark theme Black dark theme Harmonize category color for Dynamic Color + Sort by + Added time (descending) + Added time (ascending) + Uploaded time (descending) + Uploaded time (ascending) + Title (ascending) + Title (descending) + Download label (ascending) + Download label (descending) Filter All Start all (reversed)