diff --git a/app/schemas/com.hippo.ehviewer.dao.EhDatabase/23.json b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/23.json new file mode 100644 index 000000000..9e57406c6 --- /dev/null +++ b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/23.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt index 47aed5f13..87e5a91a2 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt @@ -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) { + 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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt index 2438a485b..c94904e06 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.remember import com.hippo.ehviewer.client.CHROME_USER_AGENT import com.hippo.ehviewer.client.EhUtils import com.hippo.ehviewer.client.data.FavListUrlBuilder +import com.hippo.ehviewer.download.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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt index 1b6c283af..ccea2d510 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt @@ -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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadArtist.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadArtist.kt new file mode 100644 index 000000000..6e296be49 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadArtist.kt @@ -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) = artists.map { DownloadArtist(gid, it) } + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadArtistDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadArtistDao.kt new file mode 100644 index 000000000..d4db68150 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadArtistDao.kt @@ -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) + + @Query("DELETE FROM DOWNLOAD_ARTISTS WHERE GID = :gid") + suspend fun deleteByGid(gid: Long) +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt index 217c68720..c008297ee 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadInfo.kt @@ -67,9 +67,13 @@ data class DownloadInfo( @ColumnInfo(name = "DIRNAME") val dirname: String?, + @Relation(parentColumn = "GID", entityColumn = "GID") + var artistInfoList: List = 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 + get() = artistInfoList.map { it.artist } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt index c1a39b2ae..4662b508b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DownloadsDao.kt @@ -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> + fun countByLabel(): Flow> + + @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> @Query("SELECT * FROM DOWNLOADS ORDER BY TIME") suspend fun list(): List diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt index 6f7f62202..33e233ed8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -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() + 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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadsFilterMode.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadsFilterMode.kt new file mode 100644 index 000000000..9bc703773 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadsFilterMode.kt @@ -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] + } +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt index 3cc63e641..423f8a7cf 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt @@ -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) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsFilterState.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsFilterState.kt index cfd787a54..ae33f2033 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsFilterState.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsFilterState.kt @@ -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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt index 6244d0c20..4cacca62a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/DownloadsScreen.kt @@ -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() } 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) } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SwipeToDismissBox2.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SwipeToDismissBox2.kt index ca5eee512..6dab18be5 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SwipeToDismissBox2.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/SwipeToDismissBox2.kt @@ -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() diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 188da7506..92fb61bb9 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -236,6 +236,10 @@ 记住下载标签 默认下载标签 已添加至下载列表 + 选择分组依据 + 自定义标签 + 作者 + 未知 添加 新标签 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 457b87672..760550f1b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -277,6 +277,10 @@ Remember download label Default download label Added to download list + Select Grouping Mode + Custom + Artist + Unknown Add New label