Implement sort modes and random walk for downloads (#767)
* Implement sort modes and random walk for downloads * Use `PrefDelegate<T>.asMutableState` instead BREAKING CHANGE: Upgrade database.
This commit is contained in:
1 parent
c57fd0a923
commit
953e216dde
25 files changed
+836
-335
No files matched your search
@@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ object EhDB {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateDownloadInfo(downloadInfo: List<DownloadInfo>) {
|
||||
suspend fun updateDownloadInfo(downloadInfo: Collection<DownloadInfo>) {
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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<DownloadLabel>
|
||||
|
||||
@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<DownloadLabel>
|
||||
|
||||
@Query("UPDATE DOWNLOAD_LABELS SET POSITION = POSITION - 1 WHERE POSITION > :position")
|
||||
|
||||
@@ -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<Map<@MapColumn("LABEL") String?, @MapColumn("COUNT") Int>>
|
||||
|
||||
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME")
|
||||
suspend fun list(): List<DownloadEntity>
|
||||
|
||||
@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<DownloadInfo>
|
||||
|
||||
@Query("UPDATE DOWNLOADS SET POSITION = POSITION - :offset WHERE POSITION > :position")
|
||||
suspend fun fill(position: Int, offset: Int = 1)
|
||||
|
||||
@Update
|
||||
suspend fun update(downloadInfo: List<DownloadEntity>)
|
||||
|
||||
|
||||
@@ -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<HistoryInfo>
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
|
||||
@@ -15,7 +15,7 @@ interface LocalFavoritesDao {
|
||||
@Query("SELECT COUNT(*) FROM LOCAL_FAVORITES")
|
||||
fun count(): Flow<Int>
|
||||
|
||||
@Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME DESC")
|
||||
@Query("SELECT * FROM LOCAL_FAVORITES ORDER BY TIME")
|
||||
suspend fun list(): List<LocalFavoriteInfo>
|
||||
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
@@ -27,7 +27,7 @@ interface LocalFavoritesDao {
|
||||
fun joinListLazy(title: String): PagingSource<Int, BaseGalleryInfo>
|
||||
|
||||
@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)")
|
||||
|
||||
@@ -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<QuickSearch>
|
||||
|
||||
@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<QuickSearch>
|
||||
|
||||
@Query("UPDATE QUICK_SEARCH SET POSITION = POSITION - 1 WHERE POSITION > :position")
|
||||
|
||||
@@ -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<DownloadInfo>
|
||||
get() = allInfoList
|
||||
|
||||
// All download info map
|
||||
private val mAllInfoMap = allInfoList.associateBy { it.gid } as MutableMap<Long, DownloadInfo>
|
||||
|
||||
// label and info list map, without default label info list
|
||||
private val map: MutableMap<String?, SnapshotStateList<DownloadInfo>>
|
||||
|
||||
// All labels without default label
|
||||
val labelList = runAssertingNotMainThread { EhDB.getAllDownloadLabelList() }.toMutableStateList()
|
||||
|
||||
// Store download info with default label
|
||||
private val defaultInfoList: SnapshotStateList<DownloadInfo>
|
||||
|
||||
// Store download info wait to start
|
||||
private val mWaitList = ArrayDeque<DownloadInfo>()
|
||||
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<String?, SnapshotStateList<DownloadInfo>>
|
||||
defaultInfoList = map.remove(null) ?: mutableStateListOf()
|
||||
labelList.forEach { (label) ->
|
||||
map[label] ?: mutableStateListOf<DownloadInfo>().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<DownloadInfo>) {
|
||||
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<DownloadLabel>) {
|
||||
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<DownloadInfo> {
|
||||
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<DownloadInfo> {
|
||||
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<DownloadInfo>.sortByPositionDescending() {
|
||||
sortByDescending { it.position }
|
||||
}
|
||||
private fun comparator() = SortMode.from(Settings.downloadSortMode.value).comparator()
|
||||
}
|
||||
|
||||
var downloadLocation: UniFile
|
||||
|
||||
@@ -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<DownloadInfo> {
|
||||
val comparator = when (field) {
|
||||
Field.GID -> compareBy { it.gid }
|
||||
Field.TITLE -> compareBy { EhUtils.getSuitableTitle(it) }
|
||||
Field.TIME -> compareBy { it.time }
|
||||
Field.LABEL -> compareBy<DownloadInfo> { 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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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<String?>(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<Long, DownloadInfo>() }
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Long, BaseGalleryInfo>() }
|
||||
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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -408,8 +408,16 @@ class DialogState {
|
||||
suspend fun showSingleChoice(
|
||||
items: List<String>,
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hippo.ehviewer.util
|
||||
|
||||
fun <T> MutableList<T>.insertWith(element: T, comparator: Comparator<T>) {
|
||||
val index = binarySearch(element, comparator).let {
|
||||
if (it < 0) -it - 1 else it
|
||||
}
|
||||
add(index, element)
|
||||
}
|
||||
@@ -456,6 +456,15 @@
|
||||
<string name="black_dark_theme">纯黑深色主题</string>
|
||||
<string name="harmonize_category_color">类别颜色适配动态配色</string>
|
||||
<string name="use_ci_update_channel">使用持续集成(CI)构建频道更新</string>
|
||||
<string name="sort_by">排序依据</string>
|
||||
<string name="added_time_desc">添加时间(降序)</string>
|
||||
<string name="added_time_asc">添加时间(升序)</string>
|
||||
<string name="uploaded_time_desc">上传时间(降序)</string>
|
||||
<string name="uploaded_time_asc">上传时间(升序)</string>
|
||||
<string name="title_asc">标题(升序)</string>
|
||||
<string name="title_desc">标题(降序)</string>
|
||||
<string name="download_label_asc">下载标签(升序)</string>
|
||||
<string name="download_label_desc">下载标签(降序)</string>
|
||||
<string name="download_filter">过滤</string>
|
||||
<string name="download_all">全部</string>
|
||||
<string name="download_start_all_reversed">全部开始(倒序)</string>
|
||||
|
||||
@@ -228,6 +228,17 @@
|
||||
<item>2</item>
|
||||
</integer-array>
|
||||
|
||||
<string-array name="download_sort_modes">
|
||||
<item>@string/added_time_desc</item>
|
||||
<item>@string/added_time_asc</item>
|
||||
<item>@string/uploaded_time_desc</item>
|
||||
<item>@string/uploaded_time_asc</item>
|
||||
<item>@string/title_asc</item>
|
||||
<item>@string/title_desc</item>
|
||||
<item>@string/download_label_asc</item>
|
||||
<item>@string/download_label_desc</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="download_state">
|
||||
<item>@string/download_all</item>
|
||||
<item>@string/download_state_none</item>
|
||||
|
||||
@@ -521,6 +521,15 @@
|
||||
<string name="dark_theme">Dark theme</string>
|
||||
<string name="black_dark_theme">Black dark theme</string>
|
||||
<string name="harmonize_category_color">Harmonize category color for Dynamic Color</string>
|
||||
<string name="sort_by">Sort by</string>
|
||||
<string name="added_time_desc">Added time (descending)</string>
|
||||
<string name="added_time_asc">Added time (ascending)</string>
|
||||
<string name="uploaded_time_desc">Uploaded time (descending)</string>
|
||||
<string name="uploaded_time_asc">Uploaded time (ascending)</string>
|
||||
<string name="title_asc">Title (ascending)</string>
|
||||
<string name="title_desc">Title (descending)</string>
|
||||
<string name="download_label_asc">Download label (ascending)</string>
|
||||
<string name="download_label_desc">Download label (descending)</string>
|
||||
<string name="download_filter">Filter</string>
|
||||
<string name="download_all">All</string>
|
||||
<string name="download_start_all_reversed">Start all (reversed)</string>
|
||||
|
||||
Reference in new issue
Block a user