From e0e5a155aa8db65c3b6ee8273018ffb7c10dbafb Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Wed, 6 Mar 2024 15:40:44 +0800 Subject: [PATCH] Initial support for local tag search BREAKING CHANGE: Upgrade database --- .../com.hippo.ehviewer.dao.EhDatabase/22.json | 525 ++++++++++++++++++ .../com/hippo/ehviewer/EhApplication.kt | 2 +- .../hippo/ehviewer/client/EhTagDatabase.kt | 2 +- .../ehviewer/client/data/BaseGalleryInfo.kt | 15 +- .../ehviewer/client/data/GalleryDetail.kt | 5 +- .../hippo/ehviewer/client/data/GalleryInfo.kt | 2 +- .../ehviewer/client/data/ListUrlBuilder.kt | 2 +- .../client/parser/GalleryApiParser.kt | 2 +- .../client/parser/GalleryDetailParser.kt | 16 +- .../kotlin/com/hippo/ehviewer/dao/Database.kt | 214 +------ .../hippo/ehviewer/dao/DatabaseMigrations.kt | 117 ++++ .../{GalleryFts.kt => GalleryEntityFts.kt} | 7 +- .../com/hippo/ehviewer/dao/HistoryDao.kt | 2 +- .../hippo/ehviewer/dao/LocalFavoritesDao.kt | 2 +- .../ehviewer/download/DownloadManager.kt | 25 +- .../com/hippo/ehviewer/spider/ComicInfo.kt | 57 +- .../com/hippo/ehviewer/spider/SpiderDen.kt | 2 +- .../ui/screen/DownloadsFilterState.kt | 5 +- .../ehviewer/ui/screen/GalleryListScreen.kt | 1 + .../ehviewer/ui/screen/SearchBarScreen.kt | 3 +- 20 files changed, 761 insertions(+), 245 deletions(-) create mode 100644 app/schemas/com.hippo.ehviewer.dao.EhDatabase/22.json create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/dao/DatabaseMigrations.kt rename app/src/main/kotlin/com/hippo/ehviewer/dao/{GalleryFts.kt => GalleryEntityFts.kt} (72%) diff --git a/app/schemas/com.hippo.ehviewer.dao.EhDatabase/22.json b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/22.json new file mode 100644 index 000000000..a6a56e406 --- /dev/null +++ b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/22.json @@ -0,0 +1,525 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "f61ae3beef3739c0de7db945d2c9a4a4", + "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": "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, 'f61ae3beef3739c0de7db945d2c9a4a4')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index f514d0a7c..be2f19982 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -112,7 +112,7 @@ class EhApplication : Application(), SingletonImageLoader.Factory { EhDB } launch { - DownloadManager.readPagesFromLocal() + DownloadManager.readMetadataFromLocal() } launch { FileUtils.cleanupDirectory(AppConfig.externalCrashDir) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt index 9af6cdb38..8ce5de737 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/EhTagDatabase.kt @@ -64,7 +64,7 @@ object EhTagDatabase : CoroutineScope { var initialized by mutableStateOf(false) fun getTranslation(prefix: String? = NAMESPACE_PREFIX, tag: String?): String? { - return tagGroups[prefix]?.get(tag)?.trim()?.takeIf { it.isNotEmpty() } + return tagGroups[prefix]?.get(tag)?.trim()?.ifEmpty { null } } private fun internalSuggestFlow( diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt index fd71d2d65..5a9f663e1 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/BaseGalleryInfo.kt @@ -20,6 +20,8 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey +import androidx.room.TypeConverter +import androidx.room.TypeConverters import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @@ -27,6 +29,7 @@ import kotlinx.serialization.Serializable @Parcelize @Serializable @Entity(tableName = "GALLERIES") +@TypeConverters(SimpleTagsConverter::class) data class BaseGalleryInfo( @PrimaryKey @ColumnInfo(name = "GID") @@ -62,8 +65,8 @@ data class BaseGalleryInfo( @Ignore override var rated: Boolean = false, - @Ignore - override var simpleTags: ArrayList? = null, + @ColumnInfo(name = "SIMPLE_TAGS") + override var simpleTags: List? = null, @ColumnInfo(name = "PAGES", defaultValue = "0") override var pages: Int = 0, @@ -88,3 +91,11 @@ data class BaseGalleryInfo( ) : GalleryInfo, Parcelable { constructor() : this(0, "") } + +class SimpleTagsConverter { + @TypeConverter + fun fromString(value: String) = value.split(", ") + + @TypeConverter + fun toString(value: List) = value.joinToString() +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryDetail.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryDetail.kt index eb982163b..57c4d9583 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryDetail.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryDetail.kt @@ -45,7 +45,7 @@ data class GalleryDetail( var torrentUrl: String? = null, var archiveUrl: String? = null, var parent: String? = null, - var newerVersions: ArrayList = ArrayList(), + var newerVersions: List = emptyList(), var visible: String? = null, var language: String? = null, var size: String? = null, @@ -56,8 +56,9 @@ data class GalleryDetail( val previewPages: Int, val previewList: List, ) : GalleryInfo by galleryInfo, Parcelable { - override fun generateSLang() { + fun fillInfo() { val index = LANGUAGES.indexOf(language) if (index != -1) simpleLanguage = S_LANGS[index] + simpleTags = tags.flatten() } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt index c31ac39e6..09fc88375 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/GalleryInfo.kt @@ -30,7 +30,7 @@ interface GalleryInfo { var disowned: Boolean var rating: Float var rated: Boolean - var simpleTags: ArrayList? + var simpleTags: List? var pages: Int var thumbWidth: Int var thumbHeight: Int diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt index d58ac8110..7a94f9065 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/data/ListUrlBuilder.kt @@ -80,7 +80,7 @@ data class ListUrlBuilder( minRating = q.minRating pageFrom = q.pageFrom pageTo = q.pageTo - mNext = q.name.substringAfterLast('@', "").takeIf { it.isNotEmpty() } + mNext = q.name.substringAfterLast('@', "").ifEmpty { null } } fun toQuickSearch(name: String): QuickSearch { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryApiParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryApiParser.kt index ab7e6869b..ad03151aa 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryApiParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryApiParser.kt @@ -60,6 +60,6 @@ object GalleryApiParser { @SerialName("filecount") val pages: Int, val rating: Float, - val tags: ArrayList, + val tags: List, ) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt index bf69e03ef..29b7516af 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/client/parser/GalleryDetailParser.kt @@ -114,8 +114,8 @@ object GalleryDetailParser { ) parseDetail(galleryDetail, document, body) - // Generate simpleLanguage for local favorites - galleryDetail.generateSLang() + // Fill info for database + galleryDetail.fillInfo() return galleryDetail } @@ -231,16 +231,10 @@ object GalleryDetailParser { // newer version d.getElementById("gnd")?.run { val dates = PATTERN_NEWER_DATE.findAll(body).map { it.groupValues[1] }.toList() - select("a").forEachIndexed { index, element -> + gd.newerVersions = select("a").mapIndexedNotNull { index, element -> val result = GalleryDetailUrlParser.parse(element.attr("href")) - if (result != null) { - val gi = BaseGalleryInfo( - result.gid, - result.token, - element.text().trim(), - posted = dates[index], - ) - gd.newerVersions.add(gi) + result?.run { + BaseGalleryInfo(gid, token, element.text().trim(), posted = dates[index]) } } } 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 8cd7cf03a..ea4806687 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt @@ -2,206 +2,37 @@ package com.hippo.ehviewer.dao import androidx.room.AutoMigration import androidx.room.Database -import androidx.room.DeleteColumn -import androidx.room.DeleteTable -import androidx.room.RenameColumn import androidx.room.RoomDatabase import androidx.room.TypeConverters -import androidx.room.migration.AutoMigrationSpec -import androidx.sqlite.db.SupportSQLiteDatabase import com.hippo.ehviewer.client.data.BaseGalleryInfo -import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED - -@DeleteTable(tableName = "BOOKMARKS") -class DropBookMarkDao : AutoMigrationSpec - -@RenameColumn(tableName = "HISTORY", fromColumnName = "MODE", toColumnName = "FAVORITE_SLOT") -class HistoryMigration : AutoMigrationSpec { - override fun onPostMigrate(db: SupportSQLiteDatabase) { - db.execSQL("UPDATE HISTORY SET FAVORITE_SLOT = FAVORITE_SLOT - 2") - } -} - -class AddPositionColumn : AutoMigrationSpec { - override fun onPostMigrate(db: SupportSQLiteDatabase) { - val needMigrationTables = arrayOf( - "QUICK_SEARCH", - "DOWNLOAD_LABELS", - "DOWNLOADS", - ) - needMigrationTables.forEach { table -> - // TODO: Rewrite this with row_number() when min sdk is 30 (SQLite 3.28.0) - db.execSQL("UPDATE $table SET POSITION = (SELECT COUNT(*) FROM $table T WHERE T.TIME < $table.TIME)") - } - } -} - -@DeleteColumn(tableName = "QUICK_SEARCH", columnName = "TIME") -@DeleteColumn(tableName = "DOWNLOAD_LABELS", columnName = "TIME") -class DropTimeColumn : AutoMigrationSpec - -class AddGalleryTable : AutoMigrationSpec { - override fun onPostMigrate(db: SupportSQLiteDatabase) { - val needMigrationTables = arrayOf( - "HISTORY", - "DOWNLOADS", - "LOCAL_FAVORITES", - ) - needMigrationTables.forEachIndexed { index, table -> - db.execSQL( - "INSERT OR IGNORE INTO GALLERIES " + - "SELECT GID, TOKEN, TITLE, TITLE_JPN, THUMB, CATEGORY, POSTED, UPLOADER, RATING, SIMPLE_LANGUAGE, " + - "${if (index == 0) "FAVORITE_SLOT" else NOT_FAVORITED} FROM $table", - ) - } - } -} - -@DeleteColumn(tableName = "HISTORY", columnName = "TOKEN") -@DeleteColumn(tableName = "HISTORY", columnName = "TITLE") -@DeleteColumn(tableName = "HISTORY", columnName = "TITLE_JPN") -@DeleteColumn(tableName = "HISTORY", columnName = "THUMB") -@DeleteColumn(tableName = "HISTORY", columnName = "CATEGORY") -@DeleteColumn(tableName = "HISTORY", columnName = "POSTED") -@DeleteColumn(tableName = "HISTORY", columnName = "UPLOADER") -@DeleteColumn(tableName = "HISTORY", columnName = "RATING") -@DeleteColumn(tableName = "HISTORY", columnName = "SIMPLE_LANGUAGE") -@DeleteColumn(tableName = "HISTORY", columnName = "FAVORITE_SLOT") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TOKEN") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE_JPN") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "THUMB") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "CATEGORY") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "POSTED") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "UPLOADER") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "RATING") -@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "SIMPLE_LANGUAGE") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "TOKEN") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE_JPN") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "THUMB") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "CATEGORY") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "POSTED") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "UPLOADER") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "RATING") -@DeleteColumn(tableName = "DOWNLOADS", columnName = "SIMPLE_LANGUAGE") -class GalleryTableMigration : AutoMigrationSpec - -class ThumbKeyMigration : AutoMigrationSpec { - override fun onPostMigrate(db: SupportSQLiteDatabase) { - val needMigrationTables = arrayOf( - "DOWNLOADS", - "HISTORY", - "LOCAL_FAVORITES", - ) - val prefixToRemove = arrayOf( - "https://ehgt.org/", - "https://s.exhentai.org/t/", - "https://exhentai.org/t/", - ) - needMigrationTables.forEach { table -> - prefixToRemove.forEach { prefix -> - db.execSQL("UPDATE $table SET thumb = SUBSTR(thumb ,LENGTH('$prefix') + 1) WHERE thumb LIKE '$prefix%'") - } - } - } -} - -@RenameColumn(tableName = "DOWNLOADS", fromColumnName = "POSITION", toColumnName = "TIME") -class DownloadsMigration : AutoMigrationSpec - -class GalleryFtsMigration : AutoMigrationSpec { - override fun onPostMigrate(db: SupportSQLiteDatabase) { - db.execSQL("INSERT INTO GalleryFts(GalleryFts) VALUES('rebuild')") - } -} @Database( entities = [ BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class, - GalleryFts::class, + GalleryEntityFts::class, ], - version = 21, + version = 22, exportSchema = true, autoMigrations = [ - AutoMigration( - from = 4, - to = 5, - spec = ThumbKeyMigration::class, - ), - AutoMigration( - from = 5, - to = 6, - ), - AutoMigration( - from = 6, - to = 7, - spec = DropBookMarkDao::class, - ), - AutoMigration( - from = 7, - to = 8, - ), - AutoMigration( - from = 8, - to = 9, - ), - AutoMigration( - from = 9, - to = 10, - spec = HistoryMigration::class, - ), - AutoMigration( - from = 10, - to = 11, - spec = AddPositionColumn::class, - ), - AutoMigration( - from = 11, - to = 12, - spec = DropTimeColumn::class, - ), - AutoMigration( - from = 12, - to = 13, - spec = AddGalleryTable::class, - ), - AutoMigration( - from = 13, - to = 14, - spec = GalleryTableMigration::class, - ), - AutoMigration( - from = 14, - to = 15, - ), - AutoMigration( - from = 15, - to = 16, - ), - AutoMigration( - from = 16, - to = 17, - ), - AutoMigration( - from = 17, - to = 18, - ), - AutoMigration( - from = 18, - to = 19, - spec = DownloadsMigration::class, - ), - AutoMigration( - from = 19, - to = 20, - ), - AutoMigration( - from = 20, - to = 21, - spec = GalleryFtsMigration::class, - ), + AutoMigration(from = 4, to = 5, spec = Schema4to5::class), + AutoMigration(from = 5, to = 6), + AutoMigration(from = 6, to = 7, spec = Schema6to7::class), + AutoMigration(from = 7, to = 8), + AutoMigration(from = 8, to = 9), + AutoMigration(from = 9, to = 10, spec = Schema9to10::class), + AutoMigration(from = 10, to = 11, spec = Schema10to11::class), + AutoMigration(from = 11, to = 12, spec = Schema11to12::class), + AutoMigration(from = 12, to = 13, spec = Schema12to13::class), + AutoMigration(from = 13, to = 14, spec = Schema13to14::class), + AutoMigration(from = 14, to = 15), + AutoMigration(from = 15, to = 16), + AutoMigration(from = 16, to = 17), + AutoMigration(from = 17, to = 18), + AutoMigration(from = 18, to = 19, spec = Schema18to19::class), + AutoMigration(from = 19, to = 20), + AutoMigration(from = 20, to = 21, spec = Schema20to21::class), + AutoMigration(from = 21, to = 22, spec = Schema21to22::class), ], ) @TypeConverters(FilterModeConverter::class) @@ -221,10 +52,7 @@ abstract class EhDatabase : RoomDatabase() { entities = [Search::class], version = 2, autoMigrations = [ - AutoMigration( - from = 1, - to = 2, - ), + AutoMigration(from = 1, to = 2), ], ) abstract class SearchDatabase : RoomDatabase() { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/DatabaseMigrations.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/DatabaseMigrations.kt new file mode 100644 index 000000000..e90d791bd --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/DatabaseMigrations.kt @@ -0,0 +1,117 @@ +package com.hippo.ehviewer.dao + +import androidx.room.DeleteColumn +import androidx.room.DeleteTable +import androidx.room.RenameColumn +import androidx.room.RenameTable +import androidx.room.migration.AutoMigrationSpec +import androidx.sqlite.db.SupportSQLiteDatabase +import com.hippo.ehviewer.client.data.GalleryInfo.Companion.NOT_FAVORITED + +class Schema4to5 : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + val needMigrationTables = arrayOf( + "DOWNLOADS", + "HISTORY", + "LOCAL_FAVORITES", + ) + val prefixToRemove = arrayOf( + "https://ehgt.org/", + "https://s.exhentai.org/t/", + "https://exhentai.org/t/", + ) + needMigrationTables.forEach { table -> + prefixToRemove.forEach { prefix -> + db.execSQL("UPDATE $table SET thumb = SUBSTR(thumb ,LENGTH('$prefix') + 1) WHERE thumb LIKE '$prefix%'") + } + } + } +} + +@DeleteTable(tableName = "BOOKMARKS") +class Schema6to7 : AutoMigrationSpec + +@RenameColumn(tableName = "HISTORY", fromColumnName = "MODE", toColumnName = "FAVORITE_SLOT") +class Schema9to10 : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + db.execSQL("UPDATE HISTORY SET FAVORITE_SLOT = FAVORITE_SLOT - 2") + } +} + +class Schema10to11 : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + val needMigrationTables = arrayOf( + "QUICK_SEARCH", + "DOWNLOAD_LABELS", + "DOWNLOADS", + ) + needMigrationTables.forEach { table -> + // TODO: Rewrite this with row_number() when min sdk is 30 (SQLite 3.28.0) + db.execSQL("UPDATE $table SET POSITION = (SELECT COUNT(*) FROM $table T WHERE T.TIME < $table.TIME)") + } + } +} + +@DeleteColumn(tableName = "QUICK_SEARCH", columnName = "TIME") +@DeleteColumn(tableName = "DOWNLOAD_LABELS", columnName = "TIME") +class Schema11to12 : AutoMigrationSpec + +class Schema12to13 : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + val needMigrationTables = arrayOf( + "HISTORY", + "DOWNLOADS", + "LOCAL_FAVORITES", + ) + needMigrationTables.forEachIndexed { index, table -> + db.execSQL( + "INSERT OR IGNORE INTO GALLERIES " + + "SELECT GID, TOKEN, TITLE, TITLE_JPN, THUMB, CATEGORY, POSTED, UPLOADER, RATING, SIMPLE_LANGUAGE, " + + "${if (index == 0) "FAVORITE_SLOT" else NOT_FAVORITED} FROM $table", + ) + } + } +} + +@DeleteColumn(tableName = "HISTORY", columnName = "TOKEN") +@DeleteColumn(tableName = "HISTORY", columnName = "TITLE") +@DeleteColumn(tableName = "HISTORY", columnName = "TITLE_JPN") +@DeleteColumn(tableName = "HISTORY", columnName = "THUMB") +@DeleteColumn(tableName = "HISTORY", columnName = "CATEGORY") +@DeleteColumn(tableName = "HISTORY", columnName = "POSTED") +@DeleteColumn(tableName = "HISTORY", columnName = "UPLOADER") +@DeleteColumn(tableName = "HISTORY", columnName = "RATING") +@DeleteColumn(tableName = "HISTORY", columnName = "SIMPLE_LANGUAGE") +@DeleteColumn(tableName = "HISTORY", columnName = "FAVORITE_SLOT") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TOKEN") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "TITLE_JPN") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "THUMB") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "CATEGORY") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "POSTED") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "UPLOADER") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "RATING") +@DeleteColumn(tableName = "LOCAL_FAVORITES", columnName = "SIMPLE_LANGUAGE") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "TOKEN") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "TITLE_JPN") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "THUMB") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "CATEGORY") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "POSTED") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "UPLOADER") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "RATING") +@DeleteColumn(tableName = "DOWNLOADS", columnName = "SIMPLE_LANGUAGE") +class Schema13to14 : AutoMigrationSpec + +@RenameColumn(tableName = "DOWNLOADS", fromColumnName = "POSITION", toColumnName = "TIME") +class Schema18to19 : AutoMigrationSpec + +class Schema20to21 : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + db.execSQL("INSERT INTO GalleryFts(GalleryFts) VALUES('rebuild')") + } +} + +// Virtual tables can't be altered, so we use rename to recreate it +@RenameTable(fromTableName = "GalleryFts", toTableName = "GALLERIES_FTS") +class Schema21to22 : AutoMigrationSpec diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/GalleryFts.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/GalleryEntityFts.kt similarity index 72% rename from app/src/main/kotlin/com/hippo/ehviewer/dao/GalleryFts.kt rename to app/src/main/kotlin/com/hippo/ehviewer/dao/GalleryEntityFts.kt index 2d0be6e19..8fb3cffcb 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/GalleryFts.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/GalleryEntityFts.kt @@ -6,11 +6,14 @@ import androidx.room.Fts4 import com.hippo.ehviewer.client.data.BaseGalleryInfo @Fts4(contentEntity = BaseGalleryInfo::class) -@Entity -class GalleryFts( +@Entity(tableName = "GALLERIES_FTS") +class GalleryEntityFts( @ColumnInfo(name = "TITLE") val title: String, @ColumnInfo(name = "TITLE_JPN") val titleJpn: String, + + @ColumnInfo(name = "SIMPLE_TAGS") + val simpleTags: String?, ) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt index a2b5af2d1..b56833ec8 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/HistoryDao.kt @@ -18,7 +18,7 @@ interface HistoryDao { @Query( """SELECT GALLERIES.* FROM HISTORY JOIN GALLERIES USING(GID) - JOIN GalleryFts ON GALLERIES.rowid = docid WHERE GalleryFts MATCH :title ORDER BY TIME DESC""", + JOIN GALLERIES_FTS ON GALLERIES.rowid = docid WHERE GALLERIES_FTS MATCH :title ORDER BY TIME DESC""", ) fun joinListLazy(title: String): PagingSource diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt index ae5c734e9..85fdc1f58 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/LocalFavoritesDao.kt @@ -22,7 +22,7 @@ interface LocalFavoritesDao { @Query( """SELECT GALLERIES.* FROM LOCAL_FAVORITES JOIN GALLERIES USING(GID) - JOIN GalleryFts ON GALLERIES.rowid = docid WHERE GalleryFts MATCH :title ORDER BY TIME DESC""", + JOIN GALLERIES_FTS ON GALLERIES.rowid = docid WHERE GALLERIES_FTS MATCH :title ORDER BY TIME DESC""", ) fun joinListLazy(title: String): PagingSource 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 680b2f975..e11633cea 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -39,6 +39,7 @@ import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener import com.hippo.ehviewer.spider.putToDownloadDir import com.hippo.ehviewer.spider.readComicInfo import com.hippo.ehviewer.spider.readCompatFromUniFile +import com.hippo.ehviewer.spider.toSimpleTags import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.insertWith import com.hippo.ehviewer.util.mapNotNull @@ -529,17 +530,23 @@ object DownloadManager : OnSpiderListener, CoroutineScope { } } - suspend fun readPagesFromLocal() { - val list = allInfoList.filter { it.pages == 0 }.parMapNotNull(concurrency = 5) { info -> + suspend fun readMetadataFromLocal() { + val list = allInfoList.filter { it.pages == 0 || it.simpleTags == null }.parMapNotNull(concurrency = 5) { info -> info.downloadDir?.run { - val pages = findFile(SPIDER_INFO_FILENAME)?.let { - readCompatFromUniFile(it)?.pages - } ?: findFile(COMIC_INFO_FILE)?.let { - readComicInfo(it)?.pageCount - } - pages?.let { - info.pages = it + val comicInfo = findFile(COMIC_INFO_FILE)?.let { readComicInfo(it) } + if (comicInfo != null) { + info.pages = comicInfo.pageCount + info.simpleTags = comicInfo.toSimpleTags() info.galleryInfo + } else if (info.pages == 0) { + findFile(SPIDER_INFO_FILENAME)?.let { + readCompatFromUniFile(it)?.run { + info.pages = pages + info.galleryInfo + } + } + } else { + null } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt index cf3106e1c..90a7686e7 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/ComicInfo.kt @@ -3,14 +3,20 @@ package com.hippo.ehviewer.spider import com.hippo.ehviewer.client.EhUrl import com.hippo.ehviewer.client.data.GalleryDetail import com.hippo.ehviewer.client.data.GalleryInfo +import com.hippo.ehviewer.client.data.SimpleTagsConverter import com.hippo.ehviewer.client.data.TagNamespace import com.hippo.unifile.UniFile import com.hippo.unifile.openInputStream import com.hippo.unifile.openOutputStream import java.io.InputStreamReader import java.io.OutputStreamWriter +import kotlinx.serialization.KSerializer import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import nl.adaptivity.xmlutil.XmlDeclMode import nl.adaptivity.xmlutil.core.XmlVersion import nl.adaptivity.xmlutil.newGenericWriter @@ -39,14 +45,15 @@ fun GalleryInfo.getComicInfo(): ComicInfo { with(TagNamespace) { when (this@getComicInfo) { is GalleryDetail -> tags.forEach { tagList -> + val list = tagList.filterNot { it == TAG_ORIGINAL || it.startsWith('_') } when (val ns = TagNamespace(tagList.groupName)) { - Artist, Cosplayer -> artists.addAll(tagList) - Group -> groups.addAll(tagList) - Character -> characters.addAll(tagList) - Parody -> tagList.forEach { tag -> if (tag != TAG_ORIGINAL) parodies.add(tag) } - Other -> otherTags.addAll(tagList) + Artist, Cosplayer -> artists.addAll(list) + Group -> groups.addAll(list) + Character -> characters.addAll(list) + Parody -> parodies.addAll(list) + Other -> otherTags.addAll(list) Female, Male, Mixed -> ns.toPrefix()?.let { prefix -> - tagList.forEach { tag -> otherTags.add("$prefix:$tag") } + list.forEach { tag -> otherTags.add("$prefix:$tag") } } else -> Unit @@ -70,18 +77,26 @@ fun GalleryInfo.getComicInfo(): ComicInfo { return ComicInfo( series = title, alternateSeries = titleJpn.takeUnless { it.isNullOrBlank() }, - writer = groups.takeUnless { it.isEmpty() }?.joinToString(), - penciller = artists.takeUnless { it.isEmpty() }?.joinToString(), - genre = otherTags.takeUnless { it.isEmpty() }?.joinToString(), + writer = groups.ifEmpty { null }, + penciller = artists.ifEmpty { null }, + genre = otherTags.ifEmpty { null }, web = EhUrl.getGalleryDetailUrl(gid, token), pageCount = pages, languageISO = simpleLanguage?.lowercase(), - characters = characters.takeUnless { it.isEmpty() }?.joinToString(), - teams = parodies.takeUnless { it.isEmpty() }?.joinToString(), + characters = characters.ifEmpty { null }, + teams = parodies.ifEmpty { null }, communityRating = "%.1f".format(rating), ) } +fun ComicInfo.toSimpleTags() = listOfNotNull( + writer, + penciller, + genre, + characters, + teams, +).flatten().ifEmpty { null } + fun ComicInfo.write(file: UniFile) { file.openOutputStream().use { it.channel.truncate(0) @@ -103,6 +118,16 @@ fun readComicInfo(file: UniFile): ComicInfo? = runCatching { } }.getOrNull() +@Suppress("ktlint:standard:annotation") +typealias SimpleTags = @Serializable(SimpleTagsSerializer::class) List + +object SimpleTagsSerializer : KSerializer { + private val converter = SimpleTagsConverter() + override val descriptor = PrimitiveSerialDescriptor("SimpleTags", PrimitiveKind.STRING) + override fun deserialize(decoder: Decoder) = converter.fromString(decoder.decodeString()) + override fun serialize(encoder: Encoder, value: SimpleTags) = encoder.encodeString(converter.toString(value)) +} + @Serializable data class ComicInfo( @XmlElement @@ -115,15 +140,15 @@ data class ComicInfo( @XmlElement @SerialName("Writer") - val writer: String?, + val writer: SimpleTags?, @XmlElement @SerialName("Penciller") - val penciller: String?, + val penciller: SimpleTags?, @XmlElement @SerialName("Genre") - val genre: String?, + val genre: SimpleTags?, @XmlElement @SerialName("Web") @@ -139,11 +164,11 @@ data class ComicInfo( @XmlElement @SerialName("Characters") - val characters: String?, + val characters: SimpleTags?, @XmlElement @SerialName("Teams") - val teams: String?, + val teams: SimpleTags?, @XmlElement @SerialName("CommunityRating") 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 46e54042e..716e30f64 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderDen.kt @@ -188,7 +188,7 @@ class SpiderDen(val info: GalleryInfo) { private suspend fun saveFromHttpResponse(index: Int, response: HttpResponse): Boolean { val url = response.request.url.toString() - val extension = MimeTypeMap.getFileExtensionFromUrl(url).takeUnless { it.isEmpty() } ?: "jpg" + val extension = MimeTypeMap.getFileExtensionFromUrl(url).ifEmpty { "jpg" } return saveResponseMeta(index, extension) { outFile -> outFile.openOutputStream().use { val chan = it.channel.apply { truncate(0) } 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 673456b51..cfd787a54 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 @@ -14,4 +14,7 @@ data class DownloadsFilterState( fun DownloadsFilterState.take(info: DownloadInfo) = (label == "" || info.label == label) && (state == -1 || info.state == state) && - with(info) { title.containsIgnoreCase(keyword) || titleJpn.containsIgnoreCase(keyword) } + 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/GalleryListScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt index 41143d936..bd45264c2 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryListScreen.kt @@ -541,6 +541,7 @@ fun GalleryListScreen(lub: ListUrlBuilder, navigator: DestinationsNavigator) = c GalleryPageUrlSuggestion(gid, pToken, page) } }, + tagNamespace = true, searchBarOffsetY = { searchBarOffsetY }, trailingIcon = { val sheetState = LocalSideSheetState.current diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt index 6bf372f3c..21cf867c4 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/SearchBarScreen.kt @@ -104,6 +104,7 @@ fun SearchBarScreen( onSearchHidden: () -> Unit, refreshState: PullToRefreshState? = null, suggestionProvider: SuggestionProvider? = null, + tagNamespace: Boolean = false, searchBarOffsetY: () -> Int, trailingIcon: @Composable () -> Unit, filter: @Composable (() -> Unit)? = null, @@ -125,7 +126,7 @@ fun SearchBarScreen( val query = searchFieldState.text.toString() var keywords = query.substringBeforeLast(' ', "") if (keywords.isNotEmpty()) keywords += ' ' - keywords += wrapTagKeyword(keyword) + keywords += if (tagNamespace) wrapTagKeyword(keyword) else keyword.substringAfter(':') if (!keywords.endsWith(':')) keywords += ' ' searchFieldState.setTextAndPlaceCursorAtEnd(keywords) }