Add Option to Select Downloads Grouping Mode (#998)

* Add Option to Select Downloads Grouping Mode

* hide Drag and Edit Button when Mode is Artist

* Revert "hide Drag and Edit Button when Mode is Artist"

some changes are not filtered when pushed

This reverts commit 319812e156f61c32250d908ed5ed22720ea0c6b7.

* hide Drag and Edit Button when Mode is Artist

* declared is typo

* use ComicInfo.penciller

* remove BaseGalleryInfo.artist

* Correct invalidate key

* reImport kotlin.math.roundToInt, it was incorrectly deleted

* use movableContentOf

* optimize imports

* use remember not rememberInVM

* Get DownloadInfo.artists from Database or `ComicInfo.xml`

* refactor

* unknown artist group

* redundant parameter

* reformat code

* SearchBar title and hint

* Translation

* Fix when initially loaded, the count of DownloadLabel is zero.

* lazy invalid

* use remember not rememberInVM

* refactor

* Fix LazyColumn not updated

* refactor

* Hide some Button in Artist Mode

* Revert

* Fix update labelList not updated UI

* Revert reFormat

* use trailing lambda

* Revert rememberUpdatedState

* Translate

* use buildMap

* Move ARTIST to DOWNLOAD_ARTIST

* Revert multi-line for chained Modifier

* Generate DownloadInfo.artistInfoList after writeComicInfo and restoreDownload

* Fixed crash after download gallery

* Use a composite key for DOWNLOAD_ARTISTS

* Make artists non-null

* Refactor

* Only update artist info when writing comic info

* Separate list for pending artist info

* Remove unused

* Put download artist when import database

* Use a composite key of GID and ARTIST

* Refactor

* Cache labelList

* named lambda parameters

* Refactor to avoid `mutex`

* follow up on `Make artists non-null`

* ArtistList order by ARTIST

* Lift EhTagDatabase check

* Translate artist in download title

* Refactor

* Refactor readMetadataFromLocal

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>

* Do not update the DB when the `artists` is empty

* Remove unused

---------

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
authored and GitHub committed 2024-04-24 11:01:53 +00:00
1 parent 7116871652
commit 490ea2b521
16 files changed
+841 -65

No files matched your search

@@ -0,0 +1,564 @@
{
"formatVersion": 1,
"database": {
"version": 23,
"identityHash": "7d1cfc4e9a9d506a73eb0520ae414d25",
"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_TAGS` TEXT, `PAGES` INTEGER NOT NULL DEFAULT 0, `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": "simpleTags",
"columnName": "SIMPLE_TAGS",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "pages",
"columnName": "PAGES",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"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": "DOWNLOAD_ARTISTS",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `ARTIST` TEXT NOT NULL, PRIMARY KEY(`GID`, `ARTIST`), FOREIGN KEY(`GID`) REFERENCES `DOWNLOADS`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "gid",
"columnName": "GID",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "artist",
"columnName": "ARTIST",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"GID",
"ARTIST"
]
},
"indices": [],
"foreignKeys": [
{
"table": "DOWNLOADS",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"GID"
],
"referencedColumns": [
"GID"
]
}
]
},
{
"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": []
},
{
"ftsVersion": "FTS4",
"ftsOptions": {
"tokenizer": "simple",
"tokenizerArgs": [],
"contentTable": "GALLERIES",
"languageIdColumnName": "",
"matchInfo": "FTS4",
"notIndexedColumns": [],
"prefixSizes": [],
"preferredOrder": "ASC"
},
"contentSyncTriggers": [
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_BEFORE_UPDATE BEFORE UPDATE ON `GALLERIES` BEGIN DELETE FROM `GALLERIES_FTS` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_BEFORE_DELETE BEFORE DELETE ON `GALLERIES` BEGIN DELETE FROM `GALLERIES_FTS` WHERE `docid`=OLD.`rowid`; END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_AFTER_UPDATE AFTER UPDATE ON `GALLERIES` BEGIN INSERT INTO `GALLERIES_FTS`(`docid`, `TITLE`, `TITLE_JPN`, `SIMPLE_TAGS`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`, NEW.`SIMPLE_TAGS`); END",
"CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_GALLERIES_FTS_AFTER_INSERT AFTER INSERT ON `GALLERIES` BEGIN INSERT INTO `GALLERIES_FTS`(`docid`, `TITLE`, `TITLE_JPN`, `SIMPLE_TAGS`) VALUES (NEW.`rowid`, NEW.`TITLE`, NEW.`TITLE_JPN`, NEW.`SIMPLE_TAGS`); END"
],
"tableName": "GALLERIES_FTS",
"createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`TITLE` TEXT NOT NULL, `TITLE_JPN` TEXT NOT NULL, `SIMPLE_TAGS` TEXT, content=`GALLERIES`)",
"fields": [
{
"fieldPath": "title",
"columnName": "TITLE",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "titleJpn",
"columnName": "TITLE_JPN",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "simpleTags",
"columnName": "SIMPLE_TAGS",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": []
},
"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, '7d1cfc4e9a9d506a73eb0520ae414d25')"
]
}
}
+14 -2
View File
@@ -20,6 +20,7 @@ import android.net.Uri
import arrow.fx.coroutines.release
import arrow.fx.coroutines.resource
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.dao.DownloadDirname
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
@@ -113,8 +114,11 @@ object EhDB {
dao.insertOrIgnore(downloadDirnameList)
}
val downloadsCount
get() = db.downloadsDao().count()
val downloadsCountByLabel
get() = db.downloadsDao().countByLabel()
val downloadsCountByArtist
get() = db.downloadsDao().countByArtist()
suspend fun getAllDownloadLabelList() = db.downloadLabelDao().list()
@@ -142,6 +146,14 @@ object EhDB {
dao.fill(raw.position)
}
suspend fun putDownloadArtist(gid: Long, artists: List<DownloadArtist>) {
if (artists.isNotEmpty()) {
val dao = db.downloadArtistDao()
dao.deleteByGid(gid)
dao.insert(artists)
}
}
suspend fun removeLocalFavorites(galleryInfo: BaseGalleryInfo) {
db.localFavoritesDao().deleteByKey(galleryInfo.gid)
deleteGalleryInfo(galleryInfo)
@@ -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.DownloadsFilterMode
import com.hippo.ehviewer.download.SortMode
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@@ -118,6 +119,7 @@ object Settings : DataStorePreferences(null) {
val security = boolPref("require_unlock", false)
val animateItems = boolPref("animate_items", true)
val recentDownloadLabel = stringOrNullPref("recent_download_label", null)
val downloadFilterMode = intPref("download_filter_mode", DownloadsFilterMode.Default.flag)
var downloadScheme by stringOrNullPref("image_scheme", null)
var downloadAuthority by stringOrNullPref("image_authority", null)
@@ -8,11 +8,11 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
@Database(
entities = [
BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class,
BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, DownloadArtist::class,
Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class,
GalleryEntityFts::class,
],
version = 22,
version = 23,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 4, to = 5, spec = Schema4to5::class),
@@ -32,11 +32,13 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
AutoMigration(from = 19, to = 20),
AutoMigration(from = 20, to = 21, spec = Schema20to21::class),
AutoMigration(from = 21, to = 22, spec = Schema21to22::class),
AutoMigration(from = 22, to = 23),
],
)
@TypeConverters(FilterModeConverter::class)
abstract class EhDatabase : RoomDatabase() {
abstract fun galleryDao(): GalleryDao
abstract fun downloadArtistDao(): DownloadArtistDao
abstract fun downloadDirnameDao(): DownloadDirnameDao
abstract fun downloadLabelDao(): DownloadLabelDao
abstract fun downloadsDao(): DownloadsDao
@@ -0,0 +1,24 @@
package com.hippo.ehviewer.dao
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
@Entity(
tableName = "DOWNLOAD_ARTISTS",
primaryKeys = ["GID", "ARTIST"],
foreignKeys = [
ForeignKey(DownloadEntity::class, ["GID"], ["GID"], onDelete = ForeignKey.CASCADE),
],
)
data class DownloadArtist(
@ColumnInfo(name = "GID")
val gid: Long,
@ColumnInfo(name = "ARTIST")
val artist: String,
) {
companion object {
fun from(gid: Long, artists: Iterable<String>) = artists.map { DownloadArtist(gid, it) }
}
}
@@ -0,0 +1,14 @@
package com.hippo.ehviewer.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface DownloadArtistDao {
@Insert
suspend fun insert(downloadArtists: List<DownloadArtist>)
@Query("DELETE FROM DOWNLOAD_ARTISTS WHERE GID = :gid")
suspend fun deleteByGid(gid: Long)
}
@@ -67,9 +67,13 @@ data class DownloadInfo(
@ColumnInfo(name = "DIRNAME")
val dirname: String?,
@Relation(parentColumn = "GID", entityColumn = "GID")
var artistInfoList: List<DownloadArtist> = emptyList(),
@Embedded
val downloadInfo: DownloadEntity = DownloadEntity(galleryInfo.gid),
) : GalleryInfo by galleryInfo, AbstractDownloadInfo by downloadInfo {
companion object {
const val STATE_INVALID = -1
const val STATE_NONE = 0
@@ -79,3 +83,6 @@ data class DownloadInfo(
const val STATE_FAILED = 4
}
}
val DownloadInfo.artists: List<String>
get() = artistInfoList.map { it.artist }
@@ -14,7 +14,11 @@ import kotlinx.coroutines.flow.Flow
interface DownloadsDao {
@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>>
fun countByLabel(): Flow<Map<@MapColumn("LABEL") String?, @MapColumn("COUNT") Int>>
@Suppress("ktlint:standard:annotation")
@Query("SELECT ARTIST, COUNT(*) AS COUNT FROM DOWNLOADS LEFT JOIN DOWNLOAD_ARTISTS USING(GID) GROUP BY ARTIST ORDER BY ARTIST")
fun countByArtist(): Flow<Map<@MapColumn("ARTIST") String?, @MapColumn("COUNT") Int>>
@Query("SELECT * FROM DOWNLOADS ORDER BY TIME")
suspend fun list(): List<DownloadEntity>
@@ -30,6 +30,7 @@ import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.data.BaseGalleryInfo
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.DownloadLabel
import com.hippo.ehviewer.image.Image
@@ -174,7 +175,6 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
info = DownloadInfo(galleryInfo, galleryInfo.putToDownloadDir())
info.label = label
info.state = DownloadInfo.STATE_WAIT
// Add to all download list and map
allInfoList.insertWith(info, comparator())
mAllInfoMap[galleryInfo.gid] = info
@@ -266,6 +266,7 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
// Save to
EhDB.putDownloadInfo(info)
EhDB.putDownloadArtist(info.gid, info.artistInfoList)
}
}
@@ -530,26 +531,63 @@ object DownloadManager : OnSpiderListener, CoroutineScope {
}
suspend fun readMetadataFromLocal() {
val list = allInfoList.filter { it.pages == 0 || it.simpleTags == null }.parMapNotNull(concurrency = 5) { info ->
val list = allInfoList.mapNotNull {
val updateGallery = it.pages == 0 || it.simpleTags == null
val updateArtist = it.artistInfoList.isEmpty()
if (updateGallery || updateArtist) {
Triple(it, updateGallery, updateArtist)
} else {
null
}
}.parMapNotNull(concurrency = 5) { (info, updateGallery, updateArtist) ->
info.downloadDir?.run {
val comicInfo = findFile(COMIC_INFO_FILE)?.let { readComicInfo(it) }
if (comicInfo != null) {
info.pages = comicInfo.pageCount
info.simpleTags = comicInfo.toSimpleTags()
info.galleryInfo
val galleryInfo = if (updateGallery) {
info.pages = comicInfo.pageCount
info.simpleTags = comicInfo.toSimpleTags()
info.galleryInfo
} else {
null
}
val artistList = if (updateArtist && comicInfo.penciller != null) {
info.artistInfoList = DownloadArtist.from(info.gid, comicInfo.penciller)
info.gid to info.artistInfoList
} else {
null
}
if (galleryInfo != null || artistList != null) {
galleryInfo to artistList
} else {
null
}
} else if (info.pages == 0) {
findFile(SPIDER_INFO_FILENAME)?.let {
val galleryInfo = findFile(SPIDER_INFO_FILENAME)?.let {
readCompatFromUniFile(it)?.run {
info.pages = pages
info.galleryInfo
}
}
galleryInfo?.let { it to null }
} else {
null
}
}
}
EhDB.updateGalleryInfo(list)
val galleryInfoList = mutableListOf<BaseGalleryInfo>()
list.forEach { (info, artists) ->
info?.let {
galleryInfoList.add(it)
}
artists?.let { (gid, updateList) ->
EhDB.putDownloadArtist(gid, updateList)
}
}
if (galleryInfoList.isNotEmpty()) EhDB.updateGalleryInfo(galleryInfoList)
}
val isIdle: Boolean
@@ -0,0 +1,26 @@
package com.hippo.ehviewer.download
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.dao.artists
enum class DownloadsFilterMode(
val flag: Int,
val take: (info: DownloadInfo, label: String?) -> Boolean,
) {
CUSTOM(flag = 0, take = { info, label ->
label == "" || info.label == label
}),
ARTIST(flag = 1, take = { info, label ->
if (null == label) {
info.artistInfoList.isEmpty()
} else {
label == "" || info.artists.contains(label)
}
}),
;
companion object {
val Default = CUSTOM
fun from(flag: Int) = DownloadsFilterMode.entries[flag]
}
}
@@ -32,6 +32,8 @@ import com.hippo.ehviewer.client.executeSafely
import com.hippo.ehviewer.client.getImageKey
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.coil.suspendEdit
import com.hippo.ehviewer.dao.DownloadArtist
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.downloadLocation
import com.hippo.ehviewer.download.tempDownloadDir
import com.hippo.ehviewer.image.UniFileSource
@@ -325,7 +327,13 @@ class SpiderDen(val info: GalleryInfo) {
EhEngine.fillGalleryListByApi(listOf(info))
}
}
info.getComicInfo().write(it)
info.getComicInfo().apply {
write(it)
DownloadManager.getDownloadInfo(gid)?.let { downloadInfo ->
downloadInfo.artistInfoList = DownloadArtist.from(gid, penciller.orEmpty())
EhDB.putDownloadArtist(gid, downloadInfo.artistInfoList)
}
}
}.onFailure {
logcat(it)
}
@@ -2,18 +2,20 @@ package com.hippo.ehviewer.ui.screen
import android.os.Parcelable
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.util.containsIgnoreCase
import kotlinx.parcelize.Parcelize
@Parcelize
data class DownloadsFilterState(
val mode: DownloadsFilterMode,
val label: String?,
val state: Int = -1,
val keyword: String = "",
) : Parcelable
fun DownloadsFilterState.take(info: DownloadInfo) =
(label == "" || info.label == label) && (state == -1 || info.state == state) &&
mode.take(info, label) && (state == -1 || info.state == state) &&
with(info) {
title.containsIgnoreCase(keyword) || titleJpn.containsIgnoreCase(keyword) ||
simpleTags?.any { it.containsIgnoreCase(keyword) } ?: false
@@ -27,6 +27,7 @@ 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.Download
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.GridView
@@ -50,6 +51,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
@@ -80,11 +82,14 @@ 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.EhTagDatabase
import com.hippo.ehviewer.client.data.TagNamespace
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.DownloadManager.downloadInfoList
import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.download.SortMode
import com.hippo.ehviewer.icons.EhIcons
import com.hippo.ehviewer.icons.big.Download
@@ -110,6 +115,7 @@ import com.hippo.ehviewer.ui.tools.rememberInVM
import com.hippo.ehviewer.ui.tools.thenIf
import com.hippo.ehviewer.util.findActivity
import com.hippo.ehviewer.util.mapToLongArray
import com.jamal.composeprefs3.ui.ifTrueThen
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
@@ -129,7 +135,8 @@ import sh.calvin.reorderable.rememberReorderableLazyColumnState
fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
var gridView by Settings.gridView.asMutableState()
var sortMode by Settings.downloadSortMode.asMutableState()
var filterState by rememberSaveable { mutableStateOf(DownloadsFilterState(Settings.recentDownloadLabel.value)) }
val filterMode by Settings.downloadFilterMode.collectAsState { DownloadsFilterMode.from(it) }
var filterState by rememberSaveable { mutableStateOf(DownloadsFilterState(filterMode, Settings.recentDownloadLabel.value)) }
var invalidateKey by rememberSaveable { mutableStateOf(false) }
var searchBarOffsetY by remember { mutableIntStateOf(0) }
val animateItems by Settings.animateItems.collectAsState()
@@ -144,18 +151,32 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
val activity = remember { findActivity<MainActivity>() }
val density = LocalDensity.current
val view = LocalView.current
val canTranslate = Settings.showTagTranslations && EhTagDatabase.isTranslatable(context) && EhTagDatabase.initialized
val ehTags = EhTagDatabase.takeIf { canTranslate }
fun String.translateArtist() = ehTags?.getTranslation(TagNamespace.Artist.toPrefix(), this) ?: this
val positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold
val allName = stringResource(R.string.download_all)
val defaultName = stringResource(R.string.default_download_label_name)
val unknownName = stringResource(R.string.unknown_artists)
val emptyLabelName = when (filterMode) {
DownloadsFilterMode.ARTIST -> unknownName
DownloadsFilterMode.CUSTOM -> defaultName
}
val title = stringResource(
R.string.scene_download_title,
with(filterState) { if (label == "") allName else label ?: defaultName },
with(filterState) {
when (label) {
"" -> allName
null -> emptyLabelName
else -> if (mode == DownloadsFilterMode.ARTIST) label.translateArtist() else label
}
},
)
val hint = stringResource(R.string.search_bar_hint, title)
val sortModes = stringArrayResource(id = R.array.download_sort_modes)
val downloadStates = stringArrayResource(id = R.array.download_state)
val list = remember(filterState, invalidateKey) {
DownloadManager.downloadInfoList.filterTo(mutableStateListOf()) { info ->
downloadInfoList.filterTo(mutableStateListOf()) { info ->
filterState.take(info)
}
}
@@ -166,8 +187,25 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
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() }
val downloadsCountGroupByArtist by rememberInVM { EhDB.downloadsCountByArtist }.collectAsState(emptyMap())
val downloadsCountGroupByLabel by rememberInVM { EhDB.downloadsCountByLabel }.collectAsState(emptyMap())
val downloadsCount = when (filterMode) {
DownloadsFilterMode.CUSTOM -> downloadsCountGroupByLabel
DownloadsFilterMode.ARTIST -> downloadsCountGroupByArtist
}
val artistList = remember(downloadsCountGroupByArtist) {
downloadsCountGroupByArtist.keys.mapNotNull { artist -> artist?.let { it to it } }
}
val labelList by remember {
derivedStateOf {
DownloadManager.labelList.map { it.id!! to it.label }
}
}
val groupList = when (filterMode) {
DownloadsFilterMode.CUSTOM -> labelList
DownloadsFilterMode.ARTIST -> artistList
}
val totalCount = remember(downloadsCountGroupByLabel) { downloadsCountGroupByLabel.values.sum() }
fun switchLabel(label: String?) {
Settings.recentDownloadLabel.value = label
@@ -185,46 +223,67 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
title = { Text(text = labelsStr) },
windowInsets = WindowInsets(0, 0, 0, 0),
actions = {
IconButton(
onClick = {
launch {
val text = awaitInputText(title = newLabel, hint = labelsStr) { text ->
when {
text.isBlank() -> labelEmpty
text == defaultName -> defaultInvalid
DownloadManager.containLabel(text) -> labelExists
else -> null
}
}
DownloadManager.addLabel(text)
}
},
) {
Icon(imageVector = Icons.Default.NewLabel, contentDescription = null)
}
val letMeSelect = stringResource(R.string.let_me_select)
IconButton(
onClick = {
launch {
val selected = if (!Settings.hasDefaultDownloadLabel) {
0
} else {
labelList.indexOfFirst { it.label == Settings.defaultDownloadLabel } + 2
}
showSelectActions(R.string.default_download_label, selected) {
onSelect(letMeSelect) {
Settings.hasDefaultDownloadLabel = false
}
onSelect(defaultName) {
Settings.hasDefaultDownloadLabel = true
Settings.defaultDownloadLabel = null
}
labelList.forEach { (label) ->
onSelect(label) {
Settings.hasDefaultDownloadLabel = true
Settings.defaultDownloadLabel = label
if (DownloadsFilterMode.CUSTOM == filterMode) {
IconButton(
onClick = {
launch {
val text = awaitInputText(title = newLabel, hint = labelsStr) { text ->
when {
text.isBlank() -> labelEmpty
text == defaultName -> defaultInvalid
DownloadManager.containLabel(text) -> labelExists
else -> null
}
}
DownloadManager.addLabel(text)
}
},
) {
Icon(imageVector = Icons.Default.NewLabel, contentDescription = null)
}
val letMeSelect = stringResource(R.string.let_me_select)
IconButton(
onClick = {
launch {
val selected = if (!Settings.hasDefaultDownloadLabel) {
0
} else {
DownloadManager.labelList.indexOfFirst { it.label == Settings.defaultDownloadLabel } + 2
}
showSelectActions(R.string.default_download_label, selected) {
onSelect(letMeSelect) {
Settings.hasDefaultDownloadLabel = false
}
onSelect(defaultName) {
Settings.hasDefaultDownloadLabel = true
Settings.defaultDownloadLabel = null
}
DownloadManager.labelList.forEach { (label) ->
onSelect(label) {
Settings.hasDefaultDownloadLabel = true
Settings.defaultDownloadLabel = label
}
}
}
}
},
) {
Icon(imageVector = Icons.Default.Download, contentDescription = null)
}
}
val custom = stringResource(R.string.select_grouping_mode_custom)
val artist = stringResource(R.string.select_grouping_mode_artist)
IconButton(
onClick = {
launch {
showSelectActions(R.string.select_grouping_mode) {
val select = { mode: DownloadsFilterMode ->
filterState = filterState.copy(mode = mode, label = "")
Settings.downloadFilterMode.value = mode.flag
Settings.recentDownloadLabel.value = ""
}
onSelect(custom) { select(DownloadsFilterMode.CUSTOM) }
onSelect(artist) { select(DownloadsFilterMode.ARTIST) }
}
}
},
@@ -235,10 +294,11 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
)
val labelsListState = rememberLazyListState()
val editEnable = DownloadsFilterMode.CUSTOM == filterMode
val reorderableLabelState = rememberReorderableLazyColumnState(labelsListState) { from, to ->
val fromPosition = from.index - 2
val toPosition = to.index - 2
labelList.apply { add(toPosition, removeAt(fromPosition)) }
DownloadManager.labelList.apply { add(toPosition, removeAt(fromPosition)) }
view.performHapticFeedback(draggingHapticFeedback)
}
var fromIndex by remember { mutableIntStateOf(-1) }
@@ -270,11 +330,12 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
tonalElevation = 1.dp,
shadowElevation = 1.dp,
headlineContent = {
Text("$defaultName [${downloadsCount.getOrDefault(null, 0)}]")
Text("$emptyLabelName [${downloadsCount.getOrDefault(null, 0)}]")
},
)
}
itemsIndexed(labelList, key = { _, item -> item.id!! }) { index, (label, _, id) ->
itemsIndexed(groupList, key = { _, (id) -> id }) { index, (id, label) ->
val item by rememberUpdatedState(label)
// Not using rememberSwipeToDismissBoxState to prevent LazyColumn from reusing it
val dismissState = remember { SwipeToDismissBoxState(SwipeToDismissBoxValue.Settled, density, positionalThreshold = positionalThreshold) }
@@ -297,10 +358,11 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
}
}
}
ReorderableItem(reorderableLabelState, key = id!!) { isDragging ->
ReorderableItem(reorderableLabelState, enabled = editEnable, key = id) { isDragging ->
SwipeToDismissBox2(
state = dismissState,
backgroundContent = {},
gesturesEnabled = editEnable,
) {
val elevation by animateDpAsState(
if (isDragging) {
@@ -318,9 +380,10 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
tonalElevation = 1.dp,
shadowElevation = elevation,
headlineContent = {
Text("$item [${downloadsCount.getOrDefault(item, 0)}]")
val name = if (filterMode == DownloadsFilterMode.ARTIST) label.translateArtist() else label
Text("$name [${downloadsCount.getOrDefault(item, 0)}]")
},
trailingContent = {
trailingContent = editEnable.ifTrueThen {
Row {
IconButton(
onClick = {
@@ -350,7 +413,7 @@ fun DownloadsScreen(navigator: DestinationsNavigator) = composing(navigator) {
if (fromIndex != -1) {
if (fromIndex != index) {
val range = if (fromIndex < index) fromIndex..index else index..fromIndex
val toUpdate = labelList.slice(range)
val toUpdate = DownloadManager.labelList.slice(range)
toUpdate.zip(range).forEach { it.first.position = it.second }
launchIO { EhDB.updateDownloadLabel(toUpdate) }
}
@@ -14,6 +14,7 @@ fun SwipeToDismissBox2(
state: SwipeToDismissBoxState,
backgroundContent: @Composable RowScope.() -> Unit,
modifier: Modifier = Modifier,
gesturesEnabled: Boolean = true,
content: @Composable RowScope.() -> Unit,
) {
val viewConfiguration = LocalViewConfiguration.current
@@ -23,6 +24,7 @@ fun SwipeToDismissBox2(
backgroundContent = backgroundContent,
modifier = modifier,
enableDismissFromStartToEnd = false,
gesturesEnabled = gesturesEnabled,
) {
CompositionLocalProvider(LocalViewConfiguration provides viewConfiguration) {
content()
@@ -236,6 +236,10 @@
<string name="remember_download_label">记住下载标签</string>
<string name="default_download_label">默认下载标签</string>
<string name="added_to_download_list">已添加至下载列表</string>
<string name="select_grouping_mode">选择分组依据</string>
<string name="select_grouping_mode_custom">自定义标签</string>
<string name="select_grouping_mode_artist">作者</string>
<string name="unknown_artists">未知</string>
<!-- Download label -->
<string name="add">添加</string>
<string name="new_label_title">新标签</string>
+4
View File
@@ -277,6 +277,10 @@
<string name="remember_download_label">Remember download label</string>
<string name="default_download_label">Default download label</string>
<string name="added_to_download_list">Added to download list</string>
<string name="select_grouping_mode">Select Grouping Mode</string>
<string name="select_grouping_mode_custom">Custom</string>
<string name="select_grouping_mode_artist">Artist</string>
<string name="unknown_artists">Unknown</string>
<!-- Download label -->
<string name="add">Add</string>
<string name="new_label_title">New label</string>