From 0c3647ef19cf3b0c5c9353fbc6b3b31e7ca95b7c Mon Sep 17 00:00:00 2001 From: "yizake1820@gmail.com" Date: Mon, 29 Dec 2025 19:54:32 +0800 Subject: [PATCH] dagger2 --- .gitignore | 1 + .idea/codeStyles/Project.xml | 138 ------ .idea/codeStyles/codeStyleConfig.xml | 5 - .idea/cpuProfilingConfigs.xml | 13 - .idea/dictionaries/user.xml | 8 - .idea/vcs.xml | 6 - .../utils/extensions/SourceExtensions.kt | 14 + core/.gitignore | 1 + .../5.json | 468 ++++++++++++++++++ .../tsukiviewer/core/db/AppDatabase.kt | 88 ++++ .../core/db/dao/BookmarkGroupDao.kt | 46 ++ .../core/db/dao/BookmarkItemDao.kt | 45 ++ .../core/db/dao/CollectionCriteriaDao.kt | 78 +++ .../tsukiviewer/core/db/dao/CollectionDao.kt | 54 ++ .../core/db/dao/CollectionDoujinDao.kt | 146 ++++++ .../core/db/dao/DoujinDetailsDao.kt | 100 ++++ .../tsukiviewer/core/db/dao/DoujinTagsDao.kt | 31 ++ .../core/db/dao/IncludedFolderDao.kt | 22 + .../core/db/dao/IncludedPathDao.kt | 24 + .../tsukiviewer/core/db/dao/RecentTabDao.kt | 32 ++ .../core/db/dao/SearchHistoryDao.kt | 28 ++ .../tsukiviewer/core/db/dao/TagDao.kt | 93 ++++ .../core/db/typeconverter/FolderConverter.kt | 17 + .../typeconverter/TagSortingModeConverter.kt | 17 + .../tsukiviewer/core/model/BookmarkGroup.kt | 25 + .../tsukiviewer/core/model/BookmarkItem.kt | 37 ++ .../tsukiviewer/core/model/Collection.kt | 26 + .../core/model/CollectionCriteria.kt | 22 + .../core/model/CollectionSearchInput.kt | 13 + .../core/model/CollectionWithCriterias.kt | 23 + .../flamyoad/tsukiviewer/core/model/Doujin.kt | 69 +++ .../tsukiviewer/core/model/DoujinDetails.kt | 41 ++ .../core/model/DoujinDetailsWithTags.kt | 21 + .../tsukiviewer/core/model/DoujinTag.kt | 14 + .../core/model/EditorHistoryItem.kt | 12 + .../tsukiviewer/core/model/IncludedFolder.kt | 27 + .../tsukiviewer/core/model/IncludedPath.kt | 13 + .../flamyoad/tsukiviewer/core/model/Logic.kt | 6 + .../tsukiviewer/core/model/RecentTab.kt | 17 + .../tsukiviewer/core/model/SearchHistory.kt | 19 + .../tsukiviewer/core/model/ShortTitle.kt | 11 + .../flamyoad/tsukiviewer/core/model/Source.kt | 6 + .../flamyoad/tsukiviewer/core/model/Tag.kt | 20 + .../tsukiviewer/core/model/TagSortingMode.kt | 21 + .../tsukiviewer/core/model/TagType.kt | 17 + .../tsukiviewer/core/model/ViewMode.kt | 10 + .../tsukiviewer/core/network/FetchHistory.kt | 9 + .../core/network/FetchPercentage.kt | 23 + .../tsukiviewer/core/network/FetchResult.kt | 10 + .../tsukiviewer/core/network/FetchStatus.kt | 9 + .../tsukiviewer/core/network/NhentaiJSON.kt | 56 +++ .../core/network/api/FakkuService.kt | 17 + .../core/network/api/HenNexusService.kt | 27 + .../tsukiviewer/core/network/api/NHService.kt | 25 + .../tsukiviewer/core/parser/HenNexusParser.kt | 117 +++++ .../core/repository/BookmarkRepository.kt | 227 +++++++++ .../core/repository/CollectionRepository.kt | 167 +++++++ .../core/repository/DoujinRepository.kt | 171 +++++++ .../core/repository/MetadataRepository.kt | 363 ++++++++++++++ .../repository/SearchHistoryRepository.kt | 49 ++ .../core/repository/TagRepository.kt | 21 + .../tsukiviewer/core/utils/ImageFileFilter.kt | 23 + .../core/utils/extensions/FileExtensions.kt | 20 + 63 files changed, 3109 insertions(+), 170 deletions(-) delete mode 100644 .idea/codeStyles/Project.xml delete mode 100644 .idea/codeStyles/codeStyleConfig.xml delete mode 100644 .idea/cpuProfilingConfigs.xml delete mode 100644 .idea/dictionaries/user.xml delete mode 100644 .idea/vcs.xml create mode 100644 app/src/main/java/com/flamyoad/tsukiviewer/utils/extensions/SourceExtensions.kt create mode 100644 core/.gitignore create mode 100644 core/schemas/com.flamyoad.tsukiviewer.core.db.AppDatabase/5.json create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/AppDatabase.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkGroupDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkItemDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionCriteriaDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDoujinDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinDetailsDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinTagsDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedFolderDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedPathDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/RecentTabDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/SearchHistoryDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/TagDao.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/FolderConverter.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/TagSortingModeConverter.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkGroup.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkItem.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/Collection.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionCriteria.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionSearchInput.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionWithCriterias.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/Doujin.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetails.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetailsWithTags.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinTag.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/EditorHistoryItem.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedFolder.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedPath.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/Logic.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/RecentTab.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/SearchHistory.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/ShortTitle.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/Source.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/Tag.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagSortingMode.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagType.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/model/ViewMode.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchHistory.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchPercentage.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchResult.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchStatus.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/NhentaiJSON.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/FakkuService.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/HenNexusService.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/NHService.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/parser/HenNexusParser.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/repository/BookmarkRepository.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/repository/CollectionRepository.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/repository/DoujinRepository.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/repository/MetadataRepository.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/repository/SearchHistoryRepository.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/repository/TagRepository.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/utils/ImageFileFilter.kt create mode 100644 core/src/main/java/com/flamyoad/tsukiviewer/core/utils/extensions/FileExtensions.kt diff --git a/.gitignore b/.gitignore index 9227cdc..29b2bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.iml .gradle /local.properties +/.idea/* /.idea/caches /.idea/libraries /.idea/modules.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 3cc336b..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
- - -
-
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/cpuProfilingConfigs.xml b/.idea/cpuProfilingConfigs.xml deleted file mode 100644 index 76902e8..0000000 --- a/.idea/cpuProfilingConfigs.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/dictionaries/user.xml b/.idea/dictionaries/user.xml deleted file mode 100644 index f74759e..0000000 --- a/.idea/dictionaries/user.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - doujin - doujins - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/flamyoad/tsukiviewer/utils/extensions/SourceExtensions.kt b/app/src/main/java/com/flamyoad/tsukiviewer/utils/extensions/SourceExtensions.kt new file mode 100644 index 0000000..6621e9b --- /dev/null +++ b/app/src/main/java/com/flamyoad/tsukiviewer/utils/extensions/SourceExtensions.kt @@ -0,0 +1,14 @@ +package com.flamyoad.tsukiviewer.utils.extensions + +import com.flamyoad.tsukiviewer.R +import com.flamyoad.tsukiviewer.core.model.Source + +/** + * Extension to get the drawable resource ID for a Source enum. + * This keeps resource references in the app module while Source enum is in core. + */ +val Source.drawableId: Int + get() = when (this) { + Source.NHentai -> R.drawable.fav_nhentai_black + // Add more sources as needed + } diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/core/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/schemas/com.flamyoad.tsukiviewer.core.db.AppDatabase/5.json b/core/schemas/com.flamyoad.tsukiviewer.core.db.AppDatabase/5.json new file mode 100644 index 0000000..b3bd04b --- /dev/null +++ b/core/schemas/com.flamyoad.tsukiviewer.core.db.AppDatabase/5.json @@ -0,0 +1,468 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "5b855660336b8dc6a105a53d4b2c5ffa", + "entities": [ + { + "tableName": "included_path", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dir` TEXT NOT NULL, PRIMARY KEY(`dir`))", + "fields": [ + { + "fieldPath": "dir", + "columnName": "dir", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "dir" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "doujin_details", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `nukeCode` INTEGER NOT NULL, `fullTitleEnglish` TEXT NOT NULL, `fullTitleJapanese` TEXT NOT NULL, `shortTitleEnglish` TEXT NOT NULL, `absolutePath` TEXT NOT NULL, `folderName` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nukeCode", + "columnName": "nukeCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fullTitleEnglish", + "columnName": "fullTitleEnglish", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fullTitleJapanese", + "columnName": "fullTitleJapanese", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "shortTitleEnglish", + "columnName": "shortTitleEnglish", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "absolutePath", + "columnName": "absolutePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "folderName", + "columnName": "folderName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tagId` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `count` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "count", + "columnName": "count", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "tagId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "doujin_tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`doujinId` INTEGER NOT NULL, `tagId` INTEGER NOT NULL, PRIMARY KEY(`doujinId`, `tagId`))", + "fields": [ + { + "fieldPath": "doujinId", + "columnName": "doujinId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tagId", + "columnName": "tagId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "doujinId", + "tagId" + ] + }, + "indices": [ + { + "name": "index_doujin_tags_doujinId_tagId", + "unique": false, + "columnNames": [ + "doujinId", + "tagId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_doujin_tags_doujinId_tagId` ON `${TABLE_NAME}` (`doujinId`, `tagId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "included_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dir` TEXT NOT NULL, `parentDir` TEXT NOT NULL, `lastName` TEXT NOT NULL, PRIMARY KEY(`dir`), FOREIGN KEY(`parentDir`) REFERENCES `included_path`(`dir`) ON UPDATE NO ACTION ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED)", + "fields": [ + { + "fieldPath": "dir", + "columnName": "dir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentDir", + "columnName": "parentDir", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastName", + "columnName": "lastName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "dir" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "included_path", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "parentDir" + ], + "referencedColumns": [ + "dir" + ] + } + ] + }, + { + "tableName": "bookmark_group", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "name" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "bookmark_item", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `absolutePath` TEXT NOT NULL, `parentName` TEXT NOT NULL, `dateAdded` INTEGER NOT NULL, FOREIGN KEY(`parentName`) REFERENCES `bookmark_group`(`name`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "absolutePath", + "columnName": "absolutePath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "parentName", + "columnName": "parentName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateAdded", + "columnName": "dateAdded", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "bookmark_group", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "parentName" + ], + "referencedColumns": [ + "name" + ] + } + ] + }, + { + "tableName": "search_history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `mustIncludeAllTags` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mustIncludeAllTags", + "columnName": "mustIncludeAllTags", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "collection", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `coverPhoto` TEXT NOT NULL, `mustHaveAllTitles` INTEGER NOT NULL, `mustHaveAllIncludedTags` INTEGER NOT NULL, `mustHaveAllExcludedTags` INTEGER NOT NULL, `minNumPages` INTEGER NOT NULL, `maxNumPages` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "coverPhoto", + "columnName": "coverPhoto", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mustHaveAllTitles", + "columnName": "mustHaveAllTitles", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mustHaveAllIncludedTags", + "columnName": "mustHaveAllIncludedTags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mustHaveAllExcludedTags", + "columnName": "mustHaveAllExcludedTags", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minNumPages", + "columnName": "minNumPages", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "maxNumPages", + "columnName": "maxNumPages", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "collection_criteria", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `collectionId` INTEGER NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `valueName` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "collectionId", + "columnName": "collectionId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "valueName", + "columnName": "valueName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recent_tabs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `title` TEXT NOT NULL, `dirPath` TEXT NOT NULL, `thumbnail` TEXT NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dirPath", + "columnName": "dirPath", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnail", + "columnName": "thumbnail", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '5b855660336b8dc6a105a53d4b2c5ffa')" + ] + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/AppDatabase.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/AppDatabase.kt new file mode 100644 index 0000000..abfffae --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/AppDatabase.kt @@ -0,0 +1,88 @@ +package com.flamyoad.tsukiviewer.core.db + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.flamyoad.tsukiviewer.core.db.dao.* +import com.flamyoad.tsukiviewer.core.model.* +import com.flamyoad.tsukiviewer.core.model.Collection + +const val DATABASE_NAME = "com.flamyoad.android.tsukiviewer.AppDatabase" + +@Database(entities = arrayOf( + IncludedPath::class, + DoujinDetails::class, + Tag::class, + DoujinTag::class, + IncludedFolder::class, + BookmarkGroup::class, + BookmarkItem::class, + SearchHistory::class, + Collection::class, + CollectionCriteria::class, + RecentTab::class + ), version = 5) + +abstract class AppDatabase: RoomDatabase() { + + abstract fun includedFolderDao(): IncludedPathDao + abstract fun doujinDetailsDao(): DoujinDetailsDao + abstract fun tagsDao(): TagDao + abstract fun doujinTagDao(): DoujinTagsDao + abstract fun folderDao(): IncludedFolderDao + abstract fun bookmarkGroupDao(): BookmarkGroupDao + abstract fun bookmarkItemDao(): BookmarkItemDao + abstract fun searchHistoryDao(): SearchHistoryDao + abstract fun collectionDao(): CollectionDao + abstract fun collectionCriteriaDao(): CollectionCriteriaDao + abstract fun collectionDoujinDao(): CollectionDoujinDao + abstract fun recentTabDao(): RecentTabDao + + companion object { + @Volatile + private var INSTANCE: AppDatabase? = null + + val MIGRATION_1_2 = object: Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `search_history` (`id` INTEGER, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `mustIncludeAllTags` INTEGER NOT NULL, PRIMARY KEY(`id`))") + } + } + + val MIGRATION_2_3 = object: Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `collection` (`id` INTEGER, `title` TEXT NOT NULL, `tags` TEXT NOT NULL, `coverPhoto` TEXT NOT NULL, PRIMARY KEY(`id`))") + } + } + + val MIGRATION_3_4 = object: Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE `collection`") + database.execSQL("CREATE TABLE IF NOT EXISTS `collection` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `coverPhoto` TEXT NOT NULL, `mustHaveAllTitles` INTEGER NOT NULL, `mustHaveAllIncludedTags` INTEGER NOT NULL, `mustHaveAllExcludedTags` INTEGER NOT NULL, `minNumPages` INTEGER NOT NULL, `maxNumPages` INTEGER NOT NULL)"); + database.execSQL("CREATE TABLE IF NOT EXISTS `collection_criteria` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `collectionId` INTEGER NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `valueName` TEXT NOT NULL)"); + } + } + + val MIGRATION_4_5 = object: Migration(4, 5) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `recent_tabs` (`id` INTEGER, `title` TEXT NOT NULL, `dirPath` TEXT NOT NULL, `thumbnail` TEXT NOT NULL, PRIMARY KEY(`id`))"); + } + } + + fun getInstance(context: Context): AppDatabase { + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + AppDatabase::class.java, + DATABASE_NAME) + .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) + .build() + + INSTANCE = instance + instance // return instance + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkGroupDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkGroupDao.kt new file mode 100644 index 0000000..1e01bd5 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkGroupDao.kt @@ -0,0 +1,46 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import com.flamyoad.tsukiviewer.core.model.BookmarkGroup +import java.io.File + +@Dao +@TypeConverters(FolderConverter::class) +interface BookmarkGroupDao { + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(collection: BookmarkGroup) + + @Update + suspend fun update(collection: BookmarkGroup) + + @Delete + suspend fun delete(collection: BookmarkGroup) + + @Query("DELETE FROM bookmark_group WHERE name = :collectionName") + suspend fun delete(collectionName: String) + + @Query(""" + UPDATE bookmark_group + SET name = :newName + WHERE name = :oldName + """) + suspend fun changeName(oldName: String, newName: String) + + @Query("SELECT * FROM bookmark_group WHERE name = :name") + fun get(name: String): LiveData + + @Query("SELECT * FROM bookmark_group") + fun getAll(): LiveData> + + @Query("SELECT EXISTS(SELECT * FROM bookmark_group WHERE name = :name)") + fun exists(name: String): LiveData + + @Query("SELECT * FROM bookmark_group") + suspend fun getAllBlocking(): List + + @Query("SELECT parentName FROM bookmark_item WHERE absolutePath = :absolutePath") + suspend fun getCollectionNamesFrom(absolutePath: File): List +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkItemDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkItemDao.kt new file mode 100644 index 0000000..e6c8323 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/BookmarkItemDao.kt @@ -0,0 +1,45 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import com.flamyoad.tsukiviewer.core.model.BookmarkItem +import java.io.File + +@Dao +@TypeConverters(FolderConverter::class) +interface BookmarkItemDao { + + @Query("SELECT * FROM bookmark_item") + fun selectAll(): LiveData> + + @Query("SELECT * FROM bookmark_item WHERE parentName = :groupName") + fun from(groupName: String): LiveData> + + @Query("SELECT * FROM bookmark_item WHERE parentName = :groupName") + suspend fun selectFrom(groupName: String): List + + @Query(""" + SELECT EXISTS(SELECT * FROM bookmark_item + WHERE parentName = :groupName AND absolutePath = :folderPath) + """) + suspend fun exists(folderPath: File, groupName: String): Boolean + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(item: BookmarkItem): Long + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(items: List): List + + @Delete + suspend fun delete(item: BookmarkItem) + + @Delete + suspend fun delete(items: List): Int + + @Query("DELETE FROM bookmark_item WHERE absolutePath = :absolutePath AND parentName = :groupName") + suspend fun delete(absolutePath: File, groupName: String): Int + + @Query("DELETE FROM bookmark_item WHERE absolutePath = :path") + suspend fun deleteFromAllGroups(path: File): Int +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionCriteriaDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionCriteriaDao.kt new file mode 100644 index 0000000..e270b54 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionCriteriaDao.kt @@ -0,0 +1,78 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import com.flamyoad.tsukiviewer.core.model.CollectionCriteria +import com.flamyoad.tsukiviewer.core.model.Tag +import java.io.File + +@Dao +@TypeConverters(FolderConverter::class) +interface CollectionCriteriaDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(criteria: CollectionCriteria) + + @Update + suspend fun update(criteria: CollectionCriteria) + + @Query("DELETE FROM collection_criteria WHERE collectionId = :collectionId") + suspend fun delete(collectionId: Long) + + @Query(""" + SELECT value FROM collection_criteria + WHERE collectionId = :collectionId AND type = 'title' + """) + fun getTitles(collectionId: Long): LiveData> + + @Query(""" + SELECT * FROM tags + WHERE tagId IN (SELECT value + FROM COLLECTION_CRITERIA + WHERE collectionId = :collectionId AND type = 'included_tags') + """) + fun getIncludedTags(collectionId: Long): LiveData> + + @Query(""" + SELECT * FROM tags + WHERE tagId IN (SELECT value + FROM COLLECTION_CRITERIA + WHERE collectionId = :collectionId AND type = 'excluded_tags') + """) + fun getExcludedTags(collectionId: Long): LiveData> + + @Query(""" + SELECT value FROM collection_criteria + WHERE collectionId = :collectionId AND type = 'directory' + """) + fun getDirectories(collectionId: Long): LiveData> + + @Query(""" + SELECT value FROM collection_criteria + WHERE collectionId = :collectionId AND type = 'title' + """) + suspend fun getTitlesBlocking(collectionId: Long): List + + @Query(""" + SELECT * FROM tags + WHERE tagId IN (SELECT value + FROM COLLECTION_CRITERIA + WHERE collectionId = :collectionId AND type = 'included_tags') + """) + suspend fun getIncludedTagsBlocking(collectionId: Long): List + + @Query(""" + SELECT * FROM tags + WHERE tagId IN (SELECT value + FROM COLLECTION_CRITERIA + WHERE collectionId = :collectionId AND type = 'excluded_tags') + """) + suspend fun getExcludedTagsBlocking(collectionId: Long): List + + @Query(""" + SELECT value FROM collection_criteria + WHERE collectionId = :collectionId AND type = 'directory' + """) + suspend fun getDirectoriesBlocking(collectionId: Long): List +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDao.kt new file mode 100644 index 0000000..a99cdc7 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDao.kt @@ -0,0 +1,54 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import com.flamyoad.tsukiviewer.core.model.Collection +import com.flamyoad.tsukiviewer.core.model.CollectionWithCriterias +import java.io.File + +@Dao +@TypeConverters(FolderConverter::class) +interface CollectionDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(collection: Collection): Long + + @Delete + suspend fun delete(collection: Collection) + + @Query("DELETE FROM collection WHERE id = :id") + suspend fun delete(id: Long) + + @Query("SELECT * FROM collection") + fun getAll(): LiveData> + + @Query("SELECT * FROM collection") + suspend fun getAllBlocking(): List + + @Query("SELECT * FROM collection") + fun getAllWithCriterias(): LiveData> + + @Query(""" + SELECT * FROM collection + WHERE name LIKE '%' || :keyword || '%' + """) + fun getAllWithCriterias(keyword: String): LiveData> + + @Query("SELECT * FROM collection WHERE id = :collectionId") + fun get(collectionId: Long): LiveData + + @Query("SELECT * FROM collection WHERE id = :collectionId") + suspend fun getBlocking(collectionId: Long): Collection + + + @Query(""" + UPDATE collection + SET coverPhoto = :thumbnail + WHERE id = :collectionId + """) + suspend fun updateThumbnail(collectionId: Long, thumbnail: File) + + @Query("UPDATE collection SET coverPhoto = '' WHERE id = :id ") + suspend fun deleteThumbnail(id: Long) +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDoujinDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDoujinDao.kt new file mode 100644 index 0000000..c4b1b22 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/CollectionDoujinDao.kt @@ -0,0 +1,146 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.room.Dao +import androidx.room.Query +import com.flamyoad.tsukiviewer.core.model.DoujinDetails + +@Dao +interface CollectionDoujinDao { + + // Search with Included Tags (OR) + @Query(""" + SELECT * FROM doujin_details as details + INNER JOIN doujin_tags ON details.id = doujin_tags.doujinId + WHERE tagId IN (:includedTagsId) + GROUP BY details.id + """) + suspend fun searchIncludedOr(includedTagsId: List): List + + + // Search with Included Tags (AND) + @Query(""" + SELECT * FROM doujin_tags + INNER JOIN doujin_details ON doujin_tags.doujinId = doujin_details.id + WHERE doujin_tags.tagId IN (:includedTagsId) ----> Replace with list args + GROUP BY doujin_tags.doujinId + HAVING COUNT(*) = :includedTagsCount + """) + suspend fun searchIncludedAnd(includedTagsId: List, includedTagsCount: Int): List + + + // Search with Excluded Tags (OR) + @Query(""" + SELECT * FROM doujin_details WHERE id IN ( + SELECT doujinId FROM doujin_tags + EXCEPT + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:excludedTagsId) + GROUP BY doujin_tags.doujinId + ) + """) + suspend fun searchExcludedOr(excludedTagsId: List): List + + // Search with Excluded Tags (AND) + @Query(""" + SELECT * FROM doujin_details WHERE id IN ( + SELECT doujinId FROM doujin_tags + EXCEPT + SELECT doujinId FROM doujin_tags + INNER JOIN doujin_details ON doujin_tags.doujinId = doujin_details.id + WHERE doujin_tags.tagId IN (:excludedTagsId) + GROUP BY doujin_tags.doujinId + HAVING COUNT(*) = :excludedTagsCount +) + """) + suspend fun searchExcludedAnd(excludedTagsId: List, excludedTagsCount: Int): List + + + // Search with Included Tags (OR) + Excluded Tags (OR) + @Query( + """ + SELECT * FROM doujin_details + WHERE id IN ( + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:includedTagsId) + GROUP BY doujinId + EXCEPT + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:excludedTagsId) + GROUP BY doujin_tags.doujinId) + """ + ) + suspend fun searchIncludedOrExcludedOr( + includedTagsId: List, + excludedTagsId: List + ): List + + + // Search with Included Tags (OR) + Excluded Tags (AND) + @Query( + """ + SELECT * FROM doujin_details + WHERE id IN ( + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:includedTagsId) + GROUP BY doujinId + EXCEPT + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:excludedTagsId) + GROUP BY doujinId + HAVING COUNT(*) = :excludedTagsCount) -----> Replace with list size + """ + ) + suspend fun searchIncludedOrExcludedAnd( + includedTagsId: List, + excludedTagsId: List, + excludedTagsCount: Int + ): List + + + + // Search with Included Tags (AND) + Excluded Tags (OR) + @Query( + """ + SELECT * FROM doujin_details + WHERE id IN ( + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:includedTagsId) + GROUP BY doujinId + HAVING COUNT(*) = :includedTagsCount + EXCEPT + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:excludedTagsId) + GROUP BY doujinId) + """ + ) + suspend fun searchIncludedAndExcludedOr( + includedTagsId: List, + excludedTagsId: List, + includedTagsCount: Int + ): List + + + // Search with Included Tags (AND) + Excluded Tags (AND) + @Query( + """ + SELECT * FROM doujin_details + WHERE id IN ( + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:includedTagsId) + GROUP BY doujinId + HAVING COUNT(*) = :includedTagsCount + EXCEPT + SELECT doujinId FROM doujin_tags + WHERE doujin_tags.tagId IN (:excludedTagsId) + GROUP BY doujinId + HAVING COUNT(*) = :excludedTagsCount +) + """ + ) + suspend fun searchIncludedAndExcludedAnd( + includedTagsId: List, + excludedTagsId: List, + includedTagsCount: Int, + excludedTagsCount: Int + ): List +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinDetailsDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinDetailsDao.kt new file mode 100644 index 0000000..7c9957d --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinDetailsDao.kt @@ -0,0 +1,100 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import com.flamyoad.tsukiviewer.core.model.DoujinDetails +import com.flamyoad.tsukiviewer.core.model.DoujinDetailsWithTags +import com.flamyoad.tsukiviewer.core.model.ShortTitle + +// TODO: Refactor this class to two - DoujinDetailsDao and DoujinLongDetailsDao + +@Dao +@TypeConverters(FolderConverter::class) +interface DoujinDetailsDao { + + // Compile time error will occur if we change the return type to int instead of long + // Looks like Android's room defaults to long when it comes to primary keys + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(doujinDetails: DoujinDetails): Long + + @Update + suspend fun update(doujinDetails: DoujinDetails) + + @Delete + suspend fun delete(doujinDetails: DoujinDetails) + + @Query("DELETE FROM doujin_details") + suspend fun deleteAll() + + @Query("SELECT EXISTS(SELECT * FROM doujin_details WHERE fullTitleEnglish = :fullTitleEnglish)") + suspend fun existsByTitle(fullTitleEnglish: String): Boolean + + @Query("SELECT EXISTS(SELECT * FROM doujin_details WHERE absolutePath = :absolutePath)") + suspend fun existsByAbsolutePath(absolutePath: String): Boolean + + @Query("SELECT shortTitleEnglish, absolutePath FROM doujin_details") + suspend fun getAllShortTitles(): List + + @Query(""" + SELECT * FROM doujin_details + WHERE fullTitleEnglish LIKE '%' || :query || '%' OR + fullTitleJapanese LIKE '%' || :query || '%' + """) + suspend fun findByTitle(query: String): List + + @Query("SELECT * FROM doujin_details WHERE absolutePath = :absolutePath") + suspend fun findByAbsolutePath(absolutePath: String): List + + @Query("SELECT shortTitleEnglish FROM doujin_details WHERE absolutePath = :absolutePath") + fun findShortTitleByPath(absolutePath: String): List + + // SELECT * FROM doujin_tags as dt + // INNER JOIN doujin_details ON doujin_details.id = dt.doujinId + // INNER JOIN tags ON tags.tagId = dt.tagId + // WHERE name IN ('chinese', 'translated', 'dilf') + // GROUP BY doujinId + // HAVING COUNT(doujinId) = 3 + + // This method searches for doujins that have all the included tags (No more, no less) + @Query(""" + SELECT * FROM doujin_tags as dt + INNER JOIN doujin_details ON doujin_details.id = dt.doujinId + INNER JOIN tags ON tags.tagId = dt.tagId + WHERE name IN (:tags) + GROUP BY doujinId + HAVING COUNT(doujinId) = :tagCount + """) + suspend fun findByTags(tags: List, tagCount: Int): List + + // This method searches for doujins that have at least 1 of the given tags + @Query(""" + SELECT * FROM doujin_tags as dt + INNER JOIN doujin_details ON doujin_details.id = dt.doujinId + INNER JOIN tags ON tags.tagId = dt.tagId + WHERE name IN (:tags) + GROUP BY doujinId + HAVING COUNT(doujinId) = 1 + """) + suspend fun findByTags(tags: List): List + + @Query("SELECT * FROM doujin_details") + suspend fun getAllShortDetails(): List + + @Transaction + @Query("SELECT * FROM doujin_details") + suspend fun getAllLongDetails(): List + + @Transaction + @Query("SELECT * FROM doujin_details WHERE fullTitleEnglish = :fullTitle") + fun getLongDetailsByFullTitle(fullTitle: String): LiveData + + @Transaction + @Query("SELECT * FROM doujin_details WHERE absolutePath = :absolutePath") + fun getLongDetailsByPath(absolutePath: String): LiveData + + @Transaction + @Query("SELECT * FROM doujin_details WHERE absolutePath = :absolutePath") + suspend fun getLongDetailsByPathBlocking(absolutePath: String): DoujinDetailsWithTags? + +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinTagsDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinTagsDao.kt new file mode 100644 index 0000000..a448363 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/DoujinTagsDao.kt @@ -0,0 +1,31 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.room.* +import com.flamyoad.tsukiviewer.core.model.DoujinTag +import com.flamyoad.tsukiviewer.core.model.Tag + +@Dao +interface DoujinTagsDao { + + @Insert + fun insert(doujinTag: DoujinTag) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(doujinTags: List) + + @Query("DELETE FROM doujin_tags WHERE doujinId = :doujinId") + suspend fun deleteFromDoujin(doujinId: Long) + + @Query("DELETE FROM doujin_tags WHERE doujinId = :tagId") + suspend fun deleteFromTag(tagId: Long) + + @Query("DELETE FROM doujin_tags") + suspend fun deleteAll() + + @Query(""" + UPDATE tags + SET count = count - 1 + WHERE tagId IN (SELECT tagId FROM doujin_tags WHERE doujinId = :doujinId) + """) + suspend fun decrementTagCount(doujinId: Long) +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedFolderDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedFolderDao.kt new file mode 100644 index 0000000..de4ae9a --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedFolderDao.kt @@ -0,0 +1,22 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.flamyoad.tsukiviewer.core.model.IncludedFolder + +// Not used anymore +@Dao +interface IncludedFolderDao { + + @Query("SELECT * FROM included_folders") + fun getAll(): LiveData> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(folder: IncludedFolder) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(list: List) +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedPathDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedPathDao.kt new file mode 100644 index 0000000..5c89a59 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/IncludedPathDao.kt @@ -0,0 +1,24 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import com.flamyoad.tsukiviewer.core.model.IncludedPath +import java.io.File + +@Dao +@TypeConverters(FolderConverter::class) +interface IncludedPathDao { + + @Query("SELECT dir FROM included_path") + fun getAll(): LiveData> + + @Query("SELECT dir FROM included_path") + suspend fun getAllBlocking(): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(path: IncludedPath) + + @Delete + suspend fun delete(path: IncludedPath) +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/RecentTabDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/RecentTabDao.kt new file mode 100644 index 0000000..f40052e --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/RecentTabDao.kt @@ -0,0 +1,32 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import com.flamyoad.tsukiviewer.core.model.RecentTab + +@Dao +@TypeConverters(FolderConverter::class) +interface RecentTabDao { + + @Query("SELECT * FROM recent_tabs") + fun getAll(): LiveData> + + @Query("SELECT * FROM recent_tabs WHERE id = :id") + suspend fun get(id: Long): RecentTab + + @Query("SELECT * FROM recent_tabs WHERE dirPath = :path") + suspend fun getByPath(path: String): RecentTab? + + @Query("SELECT EXISTS (SELECT * FROM recent_tabs WHERE dirPath = :path)") + fun existsByPath(path: String): Boolean + + @Insert + fun insert(tab: RecentTab): Long + + @Delete + fun delete(tab: RecentTab) + + @Query("DELETE FROM recent_tabs WHERE id != :tabId") + fun deleteAllExcept(tabId: Long): Int +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/SearchHistoryDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/SearchHistoryDao.kt new file mode 100644 index 0000000..1a03f2e --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/SearchHistoryDao.kt @@ -0,0 +1,28 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import com.flamyoad.tsukiviewer.core.model.SearchHistory + +@Dao +interface SearchHistoryDao { + @Insert + fun insert(searchHistory: SearchHistory) + + @Delete + fun delete(searchHistory: SearchHistory): Int + + // Returns null if no item + @Query("SELECT * FROM search_history ORDER BY id DESC LIMIT 1") + suspend fun getLatestItem(): SearchHistory? + + @Query("SELECT * FROM search_history ORDER BY id DESC") + fun getAll(): DataSource.Factory + + @Query("DELETE FROM search_history") + fun deleteAll() +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/TagDao.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/TagDao.kt new file mode 100644 index 0000000..32050b8 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/dao/TagDao.kt @@ -0,0 +1,93 @@ +package com.flamyoad.tsukiviewer.core.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.TagSortingModeConverter +import com.flamyoad.tsukiviewer.core.model.Tag +import com.flamyoad.tsukiviewer.core.model.TagSortingMode + +@Dao +@TypeConverters(TagSortingModeConverter::class) +interface TagDao { + + @Insert + suspend fun insert(tag: Tag): Long + + @Delete + suspend fun delete(tag: Tag) + + @Query("DELETE FROM tags WHERE tagId = :tagId") + suspend fun delete(tagId: Long) + + @Query("DELETE FROM tags") + suspend fun deleteAll() + + @Query("SELECT * FROM tags ORDER BY name") + fun getAll(): LiveData> + + @Query(""" + SELECT * FROM tags + WHERE name LIKE '%' || :keyword || '%' + ORDER BY name + """) + fun getAllWithFilter(keyword: String): LiveData> + + /* + You can't use bind variables (parameters) to reference columns in the ORDER BY clause. + https://stackoverflow.com/questions/48172807/room-database-full-dynamic-query + */ + @Query(""" + SELECT * FROM tags + WHERE name LIKE '%' || :keyword || '%' + ORDER BY + CASE WHEN :sortMode = 'NAME_ASCENDING' THEN name END, + CASE WHEN :sortMode = 'NAME_DESCENDING' THEN name END DESC, + CASE WHEN :sortMode = 'COUNT_ASCENDING' THEN count END, + CASE WHEN :sortMode = 'COUNT_DESCENDING' THEN count END DESC + """) + fun getAllWithFilter(keyword: String, sortMode: TagSortingMode): LiveData> + + @Query("SELECT * FROM tags WHERE type = :category ORDER BY name") + fun getByCategory(category: String): LiveData> + + @Query(""" + SELECT * FROM tags + WHERE type = :category AND name LIKE '%' || :keyword || '%' + ORDER BY + CASE WHEN :sortMode = 'NAME_ASCENDING' THEN name END, + CASE WHEN :sortMode = 'NAME_DESCENDING' THEN name END DESC, + CASE WHEN :sortMode = 'COUNT_ASCENDING' THEN count END, + CASE WHEN :sortMode = 'COUNT_DESCENDING' THEN count END DESC + """) + fun getByCategoryWithFilter(category: String, keyword: String, sortMode: TagSortingMode): LiveData> + + @Query("SELECT * FROM tags WHERE type = :type AND name = :name") + suspend fun get(type: String, name: String): Tag? + + @Query("SELECT tagId from tags WHERE type = :type AND name = :name") + suspend fun getId(type: String, name: String): Long + + @Query("SELECT EXISTS(SELECT * FROM tags WHERE type = :type AND name = :name)") + suspend fun exists(type: String, name: String): Boolean + + @Query("UPDATE tags SET count = count + 1 WHERE type = :type AND name = :name") + suspend fun incrementCount(type: String, name: String) + + @Query("UPDATE tags SET count = count - 1 WHERE type = :type AND name = :name") + suspend fun decrementCount(type: String, name: String) + +} + +// @Query(""" +// SELECT * FROM tags +// WHERE name LIKE '%' || :keyword || '%' +// ORDER BY :sortColumn DESC +// """) +// fun getAllWithFilterDesc(keyword: String, sortColumn: String): LiveData> + +// @Query(""" +// SELECT * FROM tags +// WHERE type = :category AND name LIKE '%' || :keyword || '%' +// ORDER BY :sortColumn DESC +// """) +// fun getByCategoryWithFilterDesc(category: String, keyword: String, sortColumn: String): LiveData> \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/FolderConverter.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/FolderConverter.kt new file mode 100644 index 0000000..2850d50 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/FolderConverter.kt @@ -0,0 +1,17 @@ +package com.flamyoad.tsukiviewer.core.db.typeconverter + +import androidx.room.TypeConverter +import java.io.File + +class FolderConverter { + + @TypeConverter + fun toFolder(folderPath: String): File { + return File(folderPath) + } + + @TypeConverter + fun toString(folder: File): String { + return folder.toString() + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/TagSortingModeConverter.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/TagSortingModeConverter.kt new file mode 100644 index 0000000..ae95a3d --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/db/typeconverter/TagSortingModeConverter.kt @@ -0,0 +1,17 @@ +package com.flamyoad.tsukiviewer.core.db.typeconverter + +import androidx.room.TypeConverter +import com.flamyoad.tsukiviewer.core.model.TagSortingMode + +class TagSortingModeConverter { + + @TypeConverter + fun toName(sortMode: TagSortingMode): String { + return sortMode.toString() + } + + @TypeConverter + fun toType(typeName: String): TagSortingMode { + return TagSortingMode.valueOf(typeName) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkGroup.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkGroup.kt new file mode 100644 index 0000000..3b61522 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkGroup.kt @@ -0,0 +1,25 @@ +package com.flamyoad.tsukiviewer.core.model + +import android.net.Uri +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey + +@Entity(tableName = "bookmark_group") +data class BookmarkGroup( + @PrimaryKey val name: String, + + @Ignore var pic: Uri, + @Ignore var totalItems: Int, + @Ignore var lastDate: Long, + @Ignore var isTicked: Boolean +) { + + constructor(name: String) : this( + name, + pic = Uri.EMPTY, + totalItems = 0, + lastDate = 0, + isTicked = false + ) +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkItem.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkItem.kt new file mode 100644 index 0000000..ae3a856 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/BookmarkItem.kt @@ -0,0 +1,37 @@ +package com.flamyoad.tsukiviewer.core.model + +import android.os.Parcelable +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import java.io.File + +@Entity( + tableName = "bookmark_item", + foreignKeys = [ForeignKey( + entity = BookmarkGroup::class, + parentColumns = ["name"], + childColumns = ["parentName"], + onDelete = ForeignKey.CASCADE, + onUpdate = ForeignKey.CASCADE + )] +) +@TypeConverters(FolderConverter::class) + +data class BookmarkItem( + @PrimaryKey(autoGenerate = true) val id: Long? = null, + val absolutePath: File, + val parentName: String, + val dateAdded: Long, + @Ignore val doujin: Doujin? = null +) { + + constructor(id: Long, absolutePath: File, parentName: String, dateAdded: Long) : this( + id, + absolutePath, + parentName, + dateAdded, + null + ) + + @Ignore var isSelected: Boolean = false +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Collection.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Collection.kt new file mode 100644 index 0000000..25828e8 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Collection.kt @@ -0,0 +1,26 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Entity +import androidx.room.Ignore +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import java.io.File + +@Entity(tableName = "collection") +@TypeConverters(FolderConverter::class) + +data class Collection( + @PrimaryKey(autoGenerate = true) val id: Long? = null, + + val name: String, + val coverPhoto: File, + + // If value is true, use AND logic for filtering, otherwise use OR logic + val mustHaveAllTitles: Boolean, + val mustHaveAllIncludedTags: Boolean, + val mustHaveAllExcludedTags: Boolean, + + val minNumPages: Int, + val maxNumPages: Int +) diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionCriteria.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionCriteria.kt new file mode 100644 index 0000000..10c0f0e --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionCriteria.kt @@ -0,0 +1,22 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "collection_criteria") +data class CollectionCriteria ( + @PrimaryKey(autoGenerate = true) val id: Long? = null, + + val collectionId: Long, + val type: String, + val value: String, + val valueName: String) + +{ + companion object { + const val TITLE = "title" + const val INCLUDED_TAGS = "included_tags" + const val EXCLUDED_TAGS = "excluded_tags" + const val DIRECTORY = "directory" + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionSearchInput.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionSearchInput.kt new file mode 100644 index 0000000..f791c47 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionSearchInput.kt @@ -0,0 +1,13 @@ +package com.flamyoad.tsukiviewer.core.model + +import java.io.File + +data class CollectionSearchInput( + val collection: Collection, + val titleKeywords: List, + val includedTags: List, + val excludedTags: List, + val minNumberPages: Int = Int.MIN_VALUE, + val maxNumberPages: Int = Int.MAX_VALUE, + val mustHaveDirs: List +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionWithCriterias.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionWithCriterias.kt new file mode 100644 index 0000000..491ed3b --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/CollectionWithCriterias.kt @@ -0,0 +1,23 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Embedded +import androidx.room.Relation + +data class CollectionWithCriterias( + @Embedded + val collection: Collection, + + @Relation(parentColumn = "id", entityColumn = "collectionId") + val criteriaList: List +) { + fun getCriteriaNames(): String { + return criteriaList.joinToString(", ") { item -> + return@joinToString when (item.type) { + CollectionCriteria.TITLE -> "+${item.valueName}" + CollectionCriteria.INCLUDED_TAGS -> "+${item.valueName}" + CollectionCriteria.EXCLUDED_TAGS -> "-${item.valueName}" + else -> "+${item.valueName}" + } + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Doujin.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Doujin.kt new file mode 100644 index 0000000..ee93fa9 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Doujin.kt @@ -0,0 +1,69 @@ +package com.flamyoad.tsukiviewer.core.model + +import android.net.Uri +import android.os.Parcelable +import androidx.core.net.toUri +import com.flamyoad.tsukiviewer.core.utils.imageExtensions +import kotlinx.parcelize.Parcelize +import java.io.File + +data class Doujin( + val pic: Uri, + val title: String, + val numberOfItems: Int, + val lastModified: Long, + val path: File +) : Comparable { // Comparable interface needs to be implemented to use fun compareBy() + + /* + https://kotlinlang.org/docs/reference/data-classes.html#properties-declared-in-the-class-body + These 2 properties are moved out of primary constructor to prevent them from being + evaluated in equals() and hashcode() + + Otherwise, duplicate items might overlap in search result because there are 2 sources of data (db, fileExplorer) + */ + var parentDir: File = File("") + var shortTitle: String = "" + var isSelected: Boolean = false + + constructor( + pic: Uri, + title: String, + numberOfItems: Int, + lastModified: Long, + path: File, + parentDir: File + ) : this(pic, title, numberOfItems, lastModified, path) { + this.parentDir = parentDir + } + + override fun compareTo(other: Doujin): Int { + return shortTitle.compareTo(other.shortTitle) + } + + companion object { + fun fromFile(currentDir: File, parentDir: File): Doujin? { + val fileList = currentDir.listFiles() ?: return null + val imageList = fileList.filter { f -> f.extension in imageExtensions } + + if (imageList.isNotEmpty()) { + + val coverImage = imageList.first().toUri() + val title = currentDir.name + val numberOfImages = imageList.size + val lastModified = currentDir.lastModified() + + val doujin = Doujin( + coverImage, + title, + numberOfImages, + lastModified, + currentDir, + parentDir + ) + return doujin + } + return null + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetails.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetails.kt new file mode 100644 index 0000000..c35e311 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetails.kt @@ -0,0 +1,41 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import java.io.File + +@Entity(tableName = "doujin_details") +@TypeConverters(FolderConverter::class) + +data class DoujinDetails( + @PrimaryKey(autoGenerate = true) + val id: Long? = null, + + val nukeCode: Int, + + val fullTitleEnglish: String, + + val fullTitleJapanese: String, + + val shortTitleEnglish: String, + + val absolutePath: File, + + val folderName: String +) { + companion object { + fun getEmptyObject(dir: File): DoujinDetails { + return DoujinDetails( + id = null, + nukeCode = -1, + shortTitleEnglish = dir.name, + fullTitleEnglish = dir.name, + fullTitleJapanese = "", + absolutePath = dir, + folderName = dir.name + ) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetailsWithTags.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetailsWithTags.kt new file mode 100644 index 0000000..e543354 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinDetailsWithTags.kt @@ -0,0 +1,21 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +data class DoujinDetailsWithTags( + @Embedded + val doujinDetails: DoujinDetails, + + @Relation( + parentColumn = "id", + entity = Tag::class, + entityColumn = "tagId", + associateBy = Junction( + value = DoujinTag::class, + parentColumn = "doujinId", + entityColumn = "tagId") + ) + val tags: List +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinTag.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinTag.kt new file mode 100644 index 0000000..022562e --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/DoujinTag.kt @@ -0,0 +1,14 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Entity +import androidx.room.Index + +@Entity(tableName = "doujin_tags", + primaryKeys = ["doujinId", "tagId"], + indices = arrayOf(Index(value = ["doujinId", "tagId"])) +) + +data class DoujinTag ( + val doujinId: Long, + val tagId: Long +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/EditorHistoryItem.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/EditorHistoryItem.kt new file mode 100644 index 0000000..1d68ecd --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/EditorHistoryItem.kt @@ -0,0 +1,12 @@ +package com.flamyoad.tsukiviewer.core.model + +data class EditorHistoryItem( + val tag: Tag, + val index: Int, + val action: Mode +) + +enum class Mode { + ADD, + REMOVE +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedFolder.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedFolder.kt new file mode 100644 index 0000000..7a17b12 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedFolder.kt @@ -0,0 +1,27 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import java.io.File + +/* ***************************************************** + Not used anymore + ***************************************************** + */ +@Entity(tableName = "included_folders", + foreignKeys = [ + ForeignKey(entity = IncludedPath::class, + parentColumns = ["dir"], + childColumns = ["parentDir"], + deferred = true, + onDelete = ForeignKey.CASCADE)]) + +@TypeConverters(FolderConverter::class) +data class IncludedFolder( + @PrimaryKey + val dir: File, + + val parentDir: File, + + val lastName: String +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedPath.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedPath.kt new file mode 100644 index 0000000..19392f0 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/IncludedPath.kt @@ -0,0 +1,13 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.* +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import java.io.File + +@Entity(tableName = "included_path") +@TypeConverters(FolderConverter::class) + +data class IncludedPath( + @PrimaryKey + val dir: File +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Logic.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Logic.kt new file mode 100644 index 0000000..dc19ed8 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Logic.kt @@ -0,0 +1,6 @@ +package com.flamyoad.tsukiviewer.core.model + +enum class Logic { + AND, + OR, +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/RecentTab.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/RecentTab.kt new file mode 100644 index 0000000..1410309 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/RecentTab.kt @@ -0,0 +1,17 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.flamyoad.tsukiviewer.core.db.typeconverter.FolderConverter +import java.io.File + +@Entity(tableName = "recent_tabs") +@TypeConverters(FolderConverter::class) + +data class RecentTab( + @PrimaryKey val id: Long? = null, + val title: String, + val dirPath: File, + val thumbnail: File +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/SearchHistory.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/SearchHistory.kt new file mode 100644 index 0000000..b1253d8 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/SearchHistory.kt @@ -0,0 +1,19 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "search_history") +data class SearchHistory( + @PrimaryKey val id: Int? = null, + + val title: String, + val tags: String, + val mustIncludeAllTags: Boolean = false +) { + fun sameWith(other: SearchHistory): Boolean { + return this.title == other.title && + this.tags == other.tags && + this.mustIncludeAllTags == other.mustIncludeAllTags + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/ShortTitle.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/ShortTitle.kt new file mode 100644 index 0000000..c05812b --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/ShortTitle.kt @@ -0,0 +1,11 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.ColumnInfo +import java.io.File + +// Class containing 2 columns from @DoujinDetails. The purpose of this class is to contain the necessary +// columns needed for sorting by beautified name in LocalDoujinsFragment +data class ShortTitle( + @ColumnInfo(name = "shortTitleEnglish") val shortTitleEnglish: String, + @ColumnInfo(name = "absolutePath") val path: File +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Source.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Source.kt new file mode 100644 index 0000000..ebd2266 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Source.kt @@ -0,0 +1,6 @@ +package com.flamyoad.tsukiviewer.core.model + +enum class Source(val readableName: String, val secondPerRequest: Int) { + NHentai("NHentai", 2), +// HentaiNexus("Hentai Nexus", 5), +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Tag.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Tag.kt new file mode 100644 index 0000000..629cc6d --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/Tag.kt @@ -0,0 +1,20 @@ +package com.flamyoad.tsukiviewer.core.model + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "tags") +data class Tag ( + @PrimaryKey(autoGenerate = true) + val tagId: Long? = null, + + val type: String, + + val name: String, + + val url: String = "", + + @ColumnInfo(name = "count") + val count: Int = 1 +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagSortingMode.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagSortingMode.kt new file mode 100644 index 0000000..f430a3e --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagSortingMode.kt @@ -0,0 +1,21 @@ +package com.flamyoad.tsukiviewer.core.model + +enum class TagSortingMode(private val description: String) { + NAME_ASCENDING("By name ascending"), + NAME_DESCENDING("By name descending"), + COUNT_ASCENDING("By count ascending"), + COUNT_DESCENDING("By count descending"); + + fun getDescription(): String { + return description + } + + companion object { + private val modeByDescription = + TagSortingMode.values().associateBy(TagSortingMode::description) + + fun fromDescription(desc: String): TagSortingMode { + return modeByDescription[desc] ?: NAME_ASCENDING + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagType.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagType.kt new file mode 100644 index 0000000..a16b5a8 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/TagType.kt @@ -0,0 +1,17 @@ +package com.flamyoad.tsukiviewer.core.model + +enum class TagType(private val shortName: String) { + All("All"), + + Parodies("parody"), + Characters("character"), + Tags("tag"), + Artists("artist"), + Groups("group"), + Languages("language"), + Categories("category"); + + fun getLowerCaseName(): String { + return shortName + } +} diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/model/ViewMode.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/ViewMode.kt new file mode 100644 index 0000000..a358964 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/model/ViewMode.kt @@ -0,0 +1,10 @@ +package com.flamyoad.tsukiviewer.core.model + +enum class ViewMode(private val num: Int) { + NORMAL_GRID(0), + MINI_GRID(1), + SCALED(2); + + fun toInt(): Int = num +} + diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchHistory.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchHistory.kt new file mode 100644 index 0000000..e4599d9 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchHistory.kt @@ -0,0 +1,9 @@ +package com.flamyoad.tsukiviewer.core.network + +import java.io.File + +data class FetchHistory( + val dir: File, + val doujinName: String, + val status: FetchStatus +) \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchPercentage.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchPercentage.kt new file mode 100644 index 0000000..e418fa2 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchPercentage.kt @@ -0,0 +1,23 @@ +package com.flamyoad.tsukiviewer.core.network + +data class FetchPercentage( + val fetched: Int, + val total: Int +) { + fun getPercent(): Int { + // Promotes first argument to floating point. If not, it can only return 0 + // e.g. 33/100 will result in 0 + val percent = (fetched.toDouble() / total) * 100 + + // Rounds up the float value to its nearest integer + return percent.toInt() + } + + fun getPercentString(): String { + return getPercent().toString() + "%" // 50% + } + + fun getProgress(): String { + return "$fetched/$total" + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchResult.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchResult.kt new file mode 100644 index 0000000..b13fa6c --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchResult.kt @@ -0,0 +1,10 @@ +package com.flamyoad.tsukiviewer.core.network + +data class FetchResult( + val metadata: Metadata? = null, + val status: FetchStatus +) { + fun getDoujinTitle(): String? { + return metadata?.result?.first()?.title?.english + } +} diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchStatus.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchStatus.kt new file mode 100644 index 0000000..28a815c --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/FetchStatus.kt @@ -0,0 +1,9 @@ +package com.flamyoad.tsukiviewer.core.network + +enum class FetchStatus { + SUCCESS, + NO_MATCH, + ALREADY_EXISTS, + NETWORK_ERROR, + NONE +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/NhentaiJSON.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/NhentaiJSON.kt new file mode 100644 index 0000000..546a44e --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/NhentaiJSON.kt @@ -0,0 +1,56 @@ +package com.flamyoad.tsukiviewer.core.network + +import androidx.annotation.Keep +import com.google.gson.annotations.SerializedName + +// List of POJOs required for Retrofit +@Keep +data class Metadata( + @SerializedName("result") val result: List, + @SerializedName("hasValue") val hasValue: Boolean = true) { + + fun getDoujinTitle(): String? { + return result.first().title.english + } + + fun getTags(): List { + return result.first().tags + } +} + +data class Result( + @SerializedName("id") val nukeCode: Int, + @SerializedName("title") val title: Title, + @SerializedName("scanlator") val scanlator: String, + @SerializedName("upload_date") val upload_date: Long, + @SerializedName("tags") val tags: List +) + +data class Title ( + + /* Example of JSON which contains null japanese field + + {"id":"328067", + "media_id":"1728054", + "title":{ + "english":"[The Jinshan] Sadistic Beauty | \u8650\u7f8e\u4eba Ch.52-53 [Chinese] [\u6c92\u6709\u6f22\u5316][Ongoing]", + "japanese":null, + "pretty":"Sadistic Beauty} + . . . . . . + } + + */ + @SerializedName("english") val english : String, + @SerializedName("japanese") val japanese : String?, // This field might be null in JSON returned + @SerializedName("pretty") val pretty : String? // Haven't encountered nulls in this field. But it might +) + +data class Tags( + @SerializedName("id") val id: Int, + @SerializedName("type") val type: String, + @SerializedName("name") val name: String, + @SerializedName("url") val url: String, + @SerializedName("count") val count: Int +) + + diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/FakkuService.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/FakkuService.kt new file mode 100644 index 0000000..45e8f10 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/FakkuService.kt @@ -0,0 +1,17 @@ +package com.flamyoad.tsukiviewer.core.network.api + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path + +interface FakkuService { + + companion object { + const val baseUrl = "https://www.fakku.net" + } + + // Example: https://www.fakku.net/search/%20Occult%20Cupid + @GET("search/{title}") + fun getSearchResult(@Path("title") encodedTitle: String): Call +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/HenNexusService.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/HenNexusService.kt new file mode 100644 index 0000000..f8dc2f3 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/HenNexusService.kt @@ -0,0 +1,27 @@ +package com.flamyoad.tsukiviewer.core.network.api + +import okhttp3.ResponseBody +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.Query + +interface HenNexusService { + + companion object { + const val baseUrl = "https://hentainexus.com/" + } + + // Example: https://hentainexus.com/?q=sanjuurou + @GET(".") + fun getSearchResult(@Query("q")title: String): Call + + + // Example: https://hentainexus.com/view/6293 + @GET("view/{id}") + fun getPageUrl(@Path("id") pageId: Int): Call + + // Example: https://hentainexus.com/view/6293 + @GET("{link}") + fun getPageUrl(@Path("link", encoded = true) relativeLink: String): Call +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/NHService.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/NHService.kt new file mode 100644 index 0000000..14522f1 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/network/api/NHService.kt @@ -0,0 +1,25 @@ +package com.flamyoad.tsukiviewer.core.network.api + +import com.flamyoad.tsukiviewer.core.network.Metadata +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Query + +interface NHService { + + companion object { + const val baseUrl = "https://nhentai.net/" + } + + /* The query parameter needs to be wrapped with double quotes to let the server know that we want exact title search + + Exact searches can be performed by wrapping terms in double quotes. + For example, "big breasts" only matches galleries with "big breasts" somewhere in the title or in tags. + + Documentation link: https://nhentai.net/info/ + */ + + // %22 decodes to " (double quote) + @GET("api/galleries/search") + fun getMetadata(@Query("query") fullTitle: String): Call +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/parser/HenNexusParser.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/parser/HenNexusParser.kt new file mode 100644 index 0000000..2c17a28 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/parser/HenNexusParser.kt @@ -0,0 +1,117 @@ +package com.flamyoad.tsukiviewer.core.parser + +import com.flamyoad.tsukiviewer.core.network.Metadata +import com.flamyoad.tsukiviewer.core.network.Result +import com.flamyoad.tsukiviewer.core.network.Tags +import com.flamyoad.tsukiviewer.core.network.Title +import org.jsoup.Jsoup +import java.util.* + +class HenNexusParser { + + private val baseUrl = "https://hentainexus.com" + + // Returns the link of the first item in result + fun getFirstItemInList(html: String): String { + val document = Jsoup.parse(html) + + val itemList = document.selectFirst(".container > .columns") ?: return "" + + val firstItem = itemList.selectFirst("div") ?: return "" + + // Example value: "/view/8808" + val relativeLink = firstItem.select("a").attr("href") + + if (relativeLink.isNotBlank()) { + return relativeLink + } else { + return "" + } + } + + // Returns the title, tags found in+ the HenNexus doujin + //todo: Surround this method with try/catch.. + fun parseItem(html: String): Metadata { + val document = Jsoup.parse(html) + + val englishTitleContainer = document.selectFirst("h1.title") + + // Means no result is found + if (englishTitleContainer == null) { + return Metadata(emptyList(), false) + } + + val englishTitle = document.selectFirst("h1.title").text() ?: "" + + val table = document.selectFirst("table.view-page-details") + val tableRows = table.select("tr") + + var artistName = "" + var language = "" + var magazine = "" + var parody = "" + var publisher = "" + + for (row in tableRows) { + // https://stackoverflow.com/questions/7985791/why-jsoup-cannot-select-td-element + // Jsoup is a HTML5 compliant parser. It cannot detect a that is not inside a + val correctedHtml = "
" + row.html() + "
" + val correctedRow = Jsoup.parse(correctedHtml) + + val rowLabel = correctedRow.selectFirst(".viewcolumn").text() + val rowContainer = correctedRow.selectFirst("a") + + // 'Pages' row does not have link and will return null + if (rowContainer != null) { + // Make the string lowercase to be consistent with NHentai e.g. Big Breasts -> big breasts + val rowValue = rowContainer.text().toLowerCase(Locale.ROOT) + when (rowLabel) { + "Artist" -> artistName = rowValue + "Language" -> language = rowValue + "Magazine" -> magazine = rowValue + "Parody" -> parody = rowValue + "Publisher" -> publisher = rowValue + } + } + } + + val spans = tableRows.select("span.tag") + + val tagList = + spans + .filter { tag -> tag.text().isNotBlank() } + .map { Tags(id = 0, type = "tag", name = it.text(), count = 0, url = "") } + .toMutableList() + + if (artistName.isNotBlank()) { + tagList.add(Tags(id = 0, type = "artist", name = artistName, count = 0, url = "")) + } + + if (language.isNotBlank()) { + tagList.add(Tags(id = 0, type = "language", name = language, count = 0, url = "")) + } + + if (magazine.isNotBlank()) { + tagList.add(Tags(id = 0, type = "group", name = magazine, count = 0, url = "")) + } + + if (parody.isNotBlank()) { + tagList.add(Tags(id = 0, type = "parody", name = parody, count = 0, url = "")) + } + + if (publisher.isNotBlank()) { + tagList.add(Tags(id = 0, type = "group", name = publisher, count = 0, url = "")) + } + + val result = Result( + nukeCode = 0, + title = Title(english = englishTitle, japanese = "", pretty = englishTitle), + scanlator = "", + upload_date = 0, + tags = tagList + ) + + val metadata = Metadata(listOf(result)) + return metadata + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/BookmarkRepository.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/BookmarkRepository.kt new file mode 100644 index 0000000..2cb7974 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/BookmarkRepository.kt @@ -0,0 +1,227 @@ +package com.flamyoad.tsukiviewer.core.repository + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.room.withTransaction +import com.flamyoad.tsukiviewer.core.db.AppDatabase +import com.flamyoad.tsukiviewer.core.db.dao.BookmarkGroupDao +import com.flamyoad.tsukiviewer.core.db.dao.BookmarkItemDao +import com.flamyoad.tsukiviewer.core.model.BookmarkGroup +import com.flamyoad.tsukiviewer.core.model.BookmarkItem +import com.flamyoad.tsukiviewer.core.model.Doujin +import org.threeten.bp.Instant +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BookmarkRepository @Inject constructor( + private val db: AppDatabase, + val groupDao: BookmarkGroupDao, + val itemDao: BookmarkItemDao +) { + companion object { + const val DEFAULT_BOOKMARK_GROUP = "Default Bookmark Group" + } + + fun getAllItems(): LiveData> { + return itemDao.selectAll() + } + + fun getAllItems(group: BookmarkGroup): LiveData> { + return itemDao.from(group.name) + } + + suspend fun getAllItemsFrom(group: BookmarkGroup): List { + return itemDao.selectFrom(group.name) + } + + suspend fun getAllItemsFrom(groupName: String): List { + return itemDao.selectFrom(groupName) + } + + fun getGroup(name: String): LiveData { + return groupDao.get(name) + } + + fun getAllGroups(): LiveData> { + return groupDao.getAll() + } + + fun groupNameExists(name: String): LiveData { + return groupDao.exists(name) + } + + suspend fun getAllGroupsFrom(absolutePath: File): List { + val collections = groupDao.getAllBlocking() + val names = groupDao.getCollectionNamesFrom(absolutePath) + + for (collection in collections) { + if (collection.name in names) { + collection.isTicked = true + } + } + return collections + } + + suspend fun changeGroupName(oldName: String, newName: String) { + return groupDao.changeName(oldName, newName) + } + + suspend fun removeGroup(group: BookmarkGroup) { + groupDao.delete(group) + } + + suspend fun removeGroup(name: String) { + groupDao.delete(name) + } + + suspend fun getAllGroupsBlocking(): List { + return groupDao.getAllBlocking() + } + + suspend fun insertGroup(collection: BookmarkGroup) { + groupDao.insert(collection) + } + + suspend fun insertItem(item: BookmarkItem) { + itemDao.insert(item) + } + + suspend fun moveItemsTo(group: BookmarkGroup, itemsToBeMoved: List): Int { + val movedItems = db.withTransaction { + itemDao.delete(itemsToBeMoved) + + val itemList = itemsToBeMoved.map { x -> + BookmarkItem( + id = null, + absolutePath = x.absolutePath, + parentName = group.name, + dateAdded = x.dateAdded + ) + } + return@withTransaction itemDao.insert(itemList) + } + return movedItems.size + } + + // Returns: Snackbar message to be shown to user indicating the number of insert and delete + suspend fun wipeAndInsertNew( + absolutePath: File, + hashMap: HashMap + ): String { + val namesOfCollectionsToRemoveFrom = hashMap + .filter { kvp -> kvp.value == false } + .map { kvp -> kvp.key } + + val dateAdded = Instant.now().toEpochMilli() + + val itemsToInsert = hashMap + .filter { kvp -> kvp.value == true } + .map { kvp -> + BookmarkItem( + id = null, + absolutePath = absolutePath, + parentName = kvp.key, + dateAdded = dateAdded + ) + } + + return db.withTransaction { + try { + var insertCount = 0 + for (item in itemsToInsert) { + if (itemDao.exists(item.absolutePath, item.parentName)) { + continue + } else { + val insertedId = itemDao.insert(item) + if (insertedId > 0) { + insertCount++ + } + } + } + + var deleteCount = 0 + for (name in namesOfCollectionsToRemoveFrom) { + val count = itemDao.delete(absolutePath, name) + deleteCount += count + } + + // Example message: Added into 1 collection. Removed from 1 collection. + val stringBuilder = StringBuilder() + + if (insertCount > 0) { + stringBuilder.append("Added into ${insertCount} ${getNoun(insertCount)}. ") + } + + if (deleteCount > 0) { + stringBuilder.append("Removed from ${deleteCount} ${getNoun(deleteCount)}") + } + + return@withTransaction stringBuilder.toString() + + } catch (e: Exception) { + Log.e("db", e.message ?: "Unknown error") + e.printStackTrace() + return@withTransaction "Failed to add or remove current doujin" + } + } + } + + suspend fun insertAllItems(doujinList: List, groupNames: List): String { + val dateAdded = Instant.now().toEpochMilli() + + val itemsToInsert = groupNames.flatMap { groupName -> + doujinList.map { doujin -> + BookmarkItem( + id = null, + absolutePath = doujin.path, + parentName = groupName, + dateAdded = dateAdded + ) + } + } + + return db.withTransaction { + var insertCount = 0 + for (item in itemsToInsert) { + if (itemDao.exists(item.absolutePath, item.parentName)) { + continue + } else { + val insertedId = itemDao.insert(item) + if (insertedId > 0) { + insertCount++ + } + } + } + + if (insertCount == 0) { + return@withTransaction "No items are bookmarked" + } + + val builder = StringBuilder() + + when { + insertCount == 1 -> builder.append("$insertCount item has been bookmarked. ") + insertCount > 1 -> builder.append("$insertCount items have been bookmarked. ") + } + + val ignoreCount = doujinList.size - insertCount + when { + ignoreCount == 1 -> builder.append("$ignoreCount duplicate item ignored") + ignoreCount > 1 -> builder.append("$ignoreCount duplicate items ignored") + } + + return@withTransaction builder.toString() + } + } + + private fun getNoun(number: Int): String { + if (number > 1) { + return "collections" + } else { + return "collection" + } + } +} + diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/CollectionRepository.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/CollectionRepository.kt new file mode 100644 index 0000000..5621f1f --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/CollectionRepository.kt @@ -0,0 +1,167 @@ +package com.flamyoad.tsukiviewer.core.repository + +import androidx.lifecycle.LiveData +import androidx.room.withTransaction +import com.flamyoad.tsukiviewer.core.db.AppDatabase +import com.flamyoad.tsukiviewer.core.db.dao.CollectionCriteriaDao +import com.flamyoad.tsukiviewer.core.db.dao.CollectionDao +import com.flamyoad.tsukiviewer.core.db.dao.CollectionDoujinDao +import com.flamyoad.tsukiviewer.core.model.* +import com.flamyoad.tsukiviewer.core.model.Collection +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CollectionRepository @Inject constructor( + private val db: AppDatabase, + private val collectionDao: CollectionDao, + private val criteriaDao: CollectionCriteriaDao, + private val collectionDoujinDao: CollectionDoujinDao +) { + + suspend fun get(id: Long): Collection { + return collectionDao.getBlocking(id) + } + + suspend fun getAll(): List { + return collectionDao.getAllBlocking() + } + + fun getAllWithCriterias(): LiveData> { + return collectionDao.getAllWithCriterias() + } + + fun getAllWithCriterias(keyword: String): LiveData> { + return collectionDao.getAllWithCriterias(keyword) + } + + suspend fun insert(collection: Collection, criterias: List) { + db.withTransaction { + val collectionId = collectionDao.insert(collection) + + // If editing existing collection, wipe all previous criterias before inserting new ones + collection.id?.let { + criteriaDao.delete(it) + collectionDao.deleteThumbnail(it) + } + + // Fill in criterias with collectionId, which is impossible to get before first inserting the collection + val completedCriterias = criterias.map { criteria -> + CollectionCriteria( + id = null, + collectionId = collectionId, + type = criteria.type, + value = criteria.value, + valueName = criteria.valueName + ) + } + + for (criteria in completedCriterias) { + criteriaDao.insert(criteria) + } + } + } + + suspend fun delete(collectionId: Long) { + db.withTransaction { + collectionDao.delete(collectionId) + criteriaDao.delete(collectionId) + } + } + + // It's ok to use INSERT for new/old because onConflictStrategy is REPLACE + suspend fun update(criterias: List) { + db.withTransaction { + for (criteria in criterias) { + criteriaDao.insert(criteria) + } + } + } + + suspend fun updateThumbnail(collectionId: Long?, file: File) { + if (collectionId == null) return + collectionDao.updateThumbnail(collectionId, file) + } + + suspend fun getTitles(id: Long): List { + return criteriaDao.getTitlesBlocking(id) + } + + suspend fun getIncludedTags(id: Long): List { + return criteriaDao.getIncludedTagsBlocking(id) + } + + suspend fun getExcludedTags(id: Long): List { + return criteriaDao.getExcludedTagsBlocking(id) + } + + suspend fun getDirectories(id: Long): List { + return criteriaDao.getDirectoriesBlocking(id) + } + + suspend fun searchIncludedOrExcludedOr( + includedTags: List, + excludedTags: List + ): List { + val includedTagsId = includedTags.map { tag -> tag.tagId ?: -1 } + val excludedTagsId = excludedTags.map { tag -> tag.tagId ?: -1 } + + if (includedTags.isEmpty()) + return collectionDoujinDao.searchExcludedOr(excludedTagsId) + + if (excludedTags.isEmpty()) + return collectionDoujinDao.searchIncludedOr(includedTagsId) + + return collectionDoujinDao.searchIncludedOrExcludedOr(includedTagsId, excludedTagsId) + } + + suspend fun searchIncludedOrExcludedAnd( + includedTags: List, + excludedTags: List + ): List { + val includedTagsId = includedTags.map { tag -> tag.tagId ?: -1 } + val excludedTagsId = excludedTags.map { tag -> tag.tagId ?: -1 } + + if (includedTags.isEmpty()) + return collectionDoujinDao.searchExcludedAnd(excludedTagsId, excludedTagsId.size) + + if (excludedTags.isEmpty()) + return collectionDoujinDao.searchIncludedOr(includedTagsId) + + return collectionDoujinDao.searchIncludedOrExcludedAnd(includedTagsId, excludedTagsId, excludedTags.size) + } + + suspend fun searchIncludedAndExcludedOr( + includedTags: List, + excludedTags: List + ): List { + val includedTagsId = includedTags.map { tag -> tag.tagId ?: -1 } + val excludedTagsId = excludedTags.map { tag -> tag.tagId ?: -1 } + + if (includedTags.isEmpty()) + return collectionDoujinDao.searchExcludedOr(excludedTagsId) + + if (excludedTags.isEmpty()) + return collectionDoujinDao.searchIncludedAnd(includedTagsId, includedTagsId.size) + + return collectionDoujinDao.searchIncludedAndExcludedOr(includedTagsId, excludedTagsId, includedTags.size) + } + + suspend fun searchIncludedAndExcludedAnd( + includedTags: List, + excludedTags: List + ): List { + val includedTagsId = includedTags.map { tag -> tag.tagId ?: -1 } + val excludedTagsId = excludedTags.map { tag -> tag.tagId ?: -1 } + + if (includedTags.isEmpty()) + return collectionDoujinDao.searchExcludedAnd(excludedTagsId, excludedTagsId.size) + + if (excludedTags.isEmpty()) + return collectionDoujinDao.searchIncludedAnd(includedTagsId, includedTagsId.size) + + return collectionDoujinDao.searchIncludedAndExcludedAnd(includedTagsId, excludedTagsId, includedTags.size, excludedTags.size) + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/DoujinRepository.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/DoujinRepository.kt new file mode 100644 index 0000000..421419a --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/DoujinRepository.kt @@ -0,0 +1,171 @@ +package com.flamyoad.tsukiviewer.core.repository + +import android.app.Application +import android.content.ContentResolver +import android.provider.MediaStore +import androidx.core.content.ContentResolverCompat +import androidx.core.net.toUri +import com.flamyoad.tsukiviewer.core.db.dao.DoujinDetailsDao +import com.flamyoad.tsukiviewer.core.db.dao.IncludedPathDao +import com.flamyoad.tsukiviewer.core.model.Doujin +import com.flamyoad.tsukiviewer.core.model.DoujinDetails +import com.flamyoad.tsukiviewer.core.utils.ImageFileFilter +import com.flamyoad.tsukiviewer.core.utils.extensions.toDoujin +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.* +import java.io.File +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DoujinRepository @Inject constructor( + private val application: Application, + private val doujinDetailsDao: DoujinDetailsDao, + private val pathDao: IncludedPathDao +) { + private val contentResolver: ContentResolver = application.contentResolver + + // Cache for the full doujin list - can be set from outside + var cachedDoujinList: MutableList? = null + + fun scanForDoujins(keyword: String, tags: String, shouldIncludeAllTags: Boolean) + : Flow { + val keywordLowerCase = keyword.toLowerCase(Locale.ROOT) + + val flowFromDatabase = searchFromDatabase(keywordLowerCase, tags, shouldIncludeAllTags) + val flowFromFileExplorer: Flow = if (tags.isBlank()) { + if (cachedDoujinList != null) { + searchFromExistingList(keywordLowerCase) + } else { + searchFromFileExplorer(keywordLowerCase) + } + } else { + emptyFlow() + } + + return flowOf(flowFromDatabase, flowFromFileExplorer).flattenMerge() + } + + private fun searchFromDatabase(keyword: String, tags: String, shouldIncludeAllTags: Boolean) + : Flow = flow { + + if (keyword.isNotBlank() && tags.isNotBlank()) { // Search using both title and tags + val tagList = tags.split(",") + .map { tagName -> tagName } + + val doujinDetailItems = when (shouldIncludeAllTags) { + true -> doujinDetailsDao.findByTags(tagList, tagList.size) // Searched items must include all tags + false -> doujinDetailsDao.findByTags(tagList) // Searched items must include at least 1 tag + } + + for (item in doujinDetailItems) { + val containsKeywordEnglish = item.fullTitleEnglish.toLowerCase(Locale.ROOT).contains(keyword) + val containsKeywordJap = item.fullTitleJapanese.contains(keyword) + + if (containsKeywordEnglish || containsKeywordJap) { + emit(item.absolutePath.toDoujin() ?: return@flow) + } + } + + } else if (keyword.isNotBlank() && tags.isBlank()) { // Search using title only + val doujinDetailItems = doujinDetailsDao.findByTitle(keyword) + + for (item in doujinDetailItems) { + emit(item.absolutePath.toDoujin() ?: return@flow) + } + + } else if (tags.isNotBlank() && keyword.isBlank()) { // Search using tags only + val tagList = tags.split(",") + .map { tagName -> tagName } + + val doujinDetailItems = when (shouldIncludeAllTags) { + true -> doujinDetailsDao.findByTags(tagList, tagList.size) + false -> doujinDetailsDao.findByTags(tagList) + } + + for (item in doujinDetailItems) { + emit(item.absolutePath.toDoujin() ?: return@flow) + } + } + } + + private fun searchFromFileExplorer(keyword: String): Flow = flow { + val includedDirs = pathDao.getAllBlocking() + for (dir in includedDirs) { + val pathName = dir.toString() + + val uri = MediaStore.Files.getContentUri("external") + + val projection = arrayOf( + MediaStore.Files.FileColumns.DATA, + MediaStore.Files.FileColumns.PARENT + ) + + val selection = + "${MediaStore.Files.FileColumns.DATA} LIKE ?" + + " AND " + + "${MediaStore.Files.FileColumns.TITLE} LIKE ?" + + val params = arrayOf( + "%" + pathName + "%", + "%" + keyword + "%" + ) + + val cursor = ContentResolverCompat.query( + contentResolver, + uri, + projection, + selection, + params, + null, + null + ) + + while (cursor.moveToNext()) { + val idSet = mutableSetOf() + + val fullPath = + cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)) + val parentId = + cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.PARENT)) + + if (idSet.add(parentId)) { + val doujinDir = File(fullPath) + + val imageList = doujinDir.listFiles(ImageFileFilter()) + + if (!imageList.isNullOrEmpty()) { + val doujin = Doujin( + pic = imageList.first().toUri(), + title = doujinDir.name, + path = doujinDir, + lastModified = doujinDir.lastModified(), + numberOfItems = imageList.size + ) + emit(doujin) + } + } + } + } + } + + private fun searchFromExistingList(keyword: String): Flow = flow { + val list = cachedDoujinList ?: return@flow + + val newList = mutableListOf() + for (doujin in list) { + if (doujin.title.toLowerCase(Locale.ROOT).contains(keyword)) { + newList.add(doujin) + emit(doujin) + } + } + } + + private fun MutableList.addIfNotNull(doujinDetails: DoujinDetails) { + val doujin = doujinDetails.absolutePath.toDoujin() + if (doujin != null) { + this.add(doujin) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/MetadataRepository.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/MetadataRepository.kt new file mode 100644 index 0000000..84e1751 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/MetadataRepository.kt @@ -0,0 +1,363 @@ +package com.flamyoad.tsukiviewer.core.repository + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.webkit.WebSettings +import android.widget.Toast +import androidx.room.withTransaction +import com.flamyoad.tsukiviewer.core.db.AppDatabase +import com.flamyoad.tsukiviewer.core.db.dao.* +import com.flamyoad.tsukiviewer.core.model.DoujinDetails +import com.flamyoad.tsukiviewer.core.model.DoujinTag +import com.flamyoad.tsukiviewer.core.model.Source +import com.flamyoad.tsukiviewer.core.model.Tag +import com.flamyoad.tsukiviewer.core.network.* +import com.flamyoad.tsukiviewer.core.network.api.FakkuService +import com.flamyoad.tsukiviewer.core.network.api.HenNexusService +import com.flamyoad.tsukiviewer.core.network.api.NHService +import com.flamyoad.tsukiviewer.core.parser.HenNexusParser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.Dispatcher +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.io.File +import java.io.IOException +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MetadataRepository @Inject constructor( + private val context: Context, + private val db: AppDatabase, + val pathDao: IncludedPathDao, + val doujinDetailsDao: DoujinDetailsDao, + val tagDao: TagDao, + val doujinTagDao: DoujinTagsDao, + val folderDao: IncludedFolderDao +) { + private lateinit var nhService: NHService + private lateinit var henNexusService: HenNexusService + private lateinit var fakkuService: FakkuService + + private val henNexusParser: HenNexusParser by lazy { HenNexusParser() } + + /* + Regex used to remove text between parentheses and brackets + Before : (C97) [Batsu Jirushi (Batsu)] AzuLan Shikoshiko Bokou Seikatsu (Azur Lane) [English] [AntaresNL667] + After : AzuLan Shikoshiko Bokou Seikatsu + */ + private val regex by lazy { + "\\[.*?]|\\(.*?\\)".toRegex() + } + + private var toast: Toast? = null + + init { + initializeNetwork() + } + + private fun initializeNetwork() { + // Nhentai API refuses the requests if the user agent is not attached. + val dispatcher = Dispatcher().apply { + maxRequests = 3 + } + + val httpClient: OkHttpClient = OkHttpClient.Builder() + .addInterceptor(object : Interceptor { + override fun intercept(chain: Interceptor.Chain): okhttp3.Response { + val request = chain.request() + .newBuilder() + .removeHeader("User-Agent") + .addHeader("User-Agent", WebSettings.getDefaultUserAgent(context)) + .build() + + return chain.proceed(request) + } + }) + .addInterceptor(HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + }) + .dispatcher(dispatcher) + .build() + + val nhBuilder = Retrofit.Builder() + .client(httpClient) + .baseUrl(NHService.baseUrl) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + nhService = nhBuilder.create(NHService::class.java) + + val henNexusBuilder = Retrofit.Builder() + .client(httpClient) + .baseUrl(HenNexusService.baseUrl) + .build() + + henNexusService = henNexusBuilder.create(HenNexusService::class.java) + +// val fakkuBuilder = Retrofit.Builder() +// .client(httpClient) +// .baseUrl(FakkuService.baseUrl) +// .build() +// +// fakkuService = fakkuBuilder.create(FakkuService::class.java) + } + + suspend fun fetchMetadata(dir: File, sources: EnumSet): FetchHistory { + return withContext(Dispatchers.IO) { + if (doujinDetailsDao.existsByTitle(dir.name) || doujinDetailsDao.existsByAbsolutePath(dir.toString())) { + return@withContext FetchHistory(dir, dir.name, FetchStatus.ALREADY_EXISTS) + + } else { + val fetchResult = requestMetadata(dir.name, sources) + val title = fetchResult.getDoujinTitle() ?: dir.name + + val metadata = fetchResult.metadata + + if (metadata == null) { + return@withContext FetchHistory(dir, title, FetchStatus.NO_MATCH) + } else { + saveMetadata(metadata, dir) + return@withContext FetchHistory(dir, title, fetchResult.status) + } + } + } + } + + private fun requestMetadata(fullTitle: String, sources: EnumSet): FetchResult { + var cleanedTitle: String = "" + + if (sources.contains(Source.NHentai)) { + // First try is attempted with title which contains all these , | [] {} symbols + val queryNhentaiWithDirName = requestFromNhentai(fullTitle) + Log.d("fetchbug", "Query NHentai with Dir Name") + if (queryNhentaiWithDirName.status == FetchStatus.SUCCESS) { + return queryNhentaiWithDirName + } + + cleanedTitle = fullTitle.replace(regex, "") + if (cleanedTitle == fullTitle) { + return queryNhentaiWithDirName // No point retrying if same string + } + + // Retry with cleaned title without symbols + Log.d("fetchbug", "Query NHentai with Cleaned Name") + val queryNhentaiWithCleanTitle = requestFromNhentai(cleanedTitle) + if (queryNhentaiWithCleanTitle.status == FetchStatus.SUCCESS) { + return queryNhentaiWithCleanTitle + } + } + +// if (sources.contains(Source.HentaiNexus)) { +// Log.d("fetchbug", "Query HentaiNexus with Cleaned Name") +// if (cleanedTitle == "") { +// cleanedTitle = fullTitle.replace(regex, "") +// } +// +// return requestFromHenNexus(cleanedTitle) +// } + + Log.d("fetchbug", "No match") + return FetchResult(null, FetchStatus.NO_MATCH) + } + + private fun requestFromNhentai(fullTitle: String): FetchResult { + try { + // Wraps query parameter with double quotes to perform exact search + val response = nhService.getMetadata("\"" + fullTitle + "\"").execute() + val json = response.body() ?: return FetchResult(status = FetchStatus.NETWORK_ERROR) + + return when (json.result.isEmpty()) { + true -> FetchResult(status = FetchStatus.NO_MATCH) + false -> FetchResult(json, status = FetchStatus.SUCCESS) + } + + } catch (e: IOException) { + showToast("Failed to fetch metadata") + return FetchResult(status = FetchStatus.NETWORK_ERROR) + } + } + + private fun requestFromHenNexus(fullTitle: String): FetchResult { + try { + val searchRequest = henNexusService.getSearchResult(fullTitle).execute() + val searchResultHtml = searchRequest.body()?.string() + + if (searchResultHtml.isNullOrBlank()) { + return FetchResult(status = FetchStatus.NO_MATCH) + } + + val firstItemLink = henNexusParser.getFirstItemInList(searchResultHtml) + + val firstItemRequest = henNexusService.getPageUrl(firstItemLink).execute() + val firstItemHtml = firstItemRequest.body()?.string() + + if (firstItemHtml.isNullOrBlank()) { + return FetchResult(status = FetchStatus.NO_MATCH) + } + + val metadata = henNexusParser.parseItem(firstItemHtml) + + if (!metadata.hasValue) { + return FetchResult(status = FetchStatus.NO_MATCH) + } else { + return FetchResult(metadata, status = FetchStatus.SUCCESS) + } + + } catch (e: IOException) { + showToast("Failed to fetch metadata") + return FetchResult(status = FetchStatus.NETWORK_ERROR) + } + } + + private suspend fun saveMetadata(metadata: Metadata, dir: File): String { + // Api might return a list of duplicate results. We only want the first one + val item = metadata.result.first() + + val doujinDetails = DoujinDetails( + nukeCode = item.nukeCode, + fullTitleEnglish = item.title.english, + fullTitleJapanese = item.title.japanese ?: "", + shortTitleEnglish = item.title.pretty ?: "", + absolutePath = dir, + folderName = dir.name + ) + + db.withTransaction { + val doujinId = doujinDetailsDao.insert(doujinDetails) + + for (tag in item.tags) { + var tagId: Long + + if (tag.name.trim() == "" || tag.name == "null") + continue + + if (tagDao.exists(tag.type, tag.name)) { + tagDao.incrementCount(tag.type, tag.name) + tagId = tagDao.getId(tag.type, tag.name) + + } else { + tagId = tagDao.insert( + Tag( + tagId = null, + name = tag.name, + type = tag.type, + url = tag.url, + count = 1 + ) + ) + } + doujinTagDao.insert(DoujinTag(doujinId, tagId)) + } + } + return item.title.english + } + + // Erases previous existing tags and adds tags edited by user + suspend fun saveEditedMetadata(doujinDetails: DoujinDetails, tags: List) { + withContext(Dispatchers.IO) { + db.withTransaction { + val absolutePath = doujinDetails.absolutePath.absolutePath + + val doujinId: Long + + // Insert into db if a record identified by its absolute path does not exist yet + if (!doujinDetailsDao.existsByAbsolutePath(absolutePath)) { + doujinId = doujinDetailsDao.insert(doujinDetails) + } else { + val fetchedItems: List = + doujinDetailsDao.findByAbsolutePath(absolutePath) + doujinId = fetchedItems.first().id ?: -1 + } + + // Decrements book count for all tags in the previous data + doujinTagDao.decrementTagCount(doujinId) + + // Removes all rows related to chosen id in the table + doujinTagDao.deleteFromDoujin(doujinId) + + // Inserts new tag if has any, increments count for tags that already exist + tags.forEach { tag -> + insertTagElseIncrement(doujinId, tag) + } + } + } + } + + private suspend fun insertTagElseIncrement(doujinId: Long, tag: Tag) { + db.withTransaction { + val tagId: Long + + if (tag.name.trim() == "" || tag.name == "null") + return@withTransaction + + if (tagDao.exists(tag.type, tag.name)) { + tagDao.incrementCount(tag.type, tag.name) + tagId = tagDao.getId(tag.type, tag.name) + + } else { + tagId = tagDao.insert( + Tag( + tagId = null, + name = tag.name, + type = tag.type, + url = tag.url, + count = 1 + ) + ) + } + doujinTagDao.insert( + DoujinTag(doujinId, tagId) + ) + } + } + + suspend fun removeMetadata(doujinDetails: DoujinDetails) { + withContext(Dispatchers.IO) { + db.withTransaction { + doujinDetailsDao.delete(doujinDetails) + doujinTagDao.deleteFromDoujin(doujinDetails.id!!) + doujinTagDao.decrementTagCount(doujinDetails.id) + } + } + } + + suspend fun resetTags(dir: File, sources: EnumSet) { + withContext(Dispatchers.IO) { +// val result = requestFromNhentai(dir.name) + val result = requestMetadata(dir.name, sources) + + if (result.status == FetchStatus.SUCCESS) { + val doujinDetails = doujinDetailsDao + .findByAbsolutePath(dir.absolutePath) + .first() + + if (result.metadata == null) return@withContext + + val tagList = result.metadata.getTags() + .map { x -> Tag(type = x.type, name = x.name, url = x.url, count = 1) } + saveEditedMetadata(doujinDetails, tagList) + } else { + Log.d("retrofit", "Can't find this sauce") + } + } + } + + // Need this because we are running in non-UI thread + private fun showToast(message: String) { + val handler = Handler(Looper.getMainLooper()) + handler.post { + toast?.cancel() + + toast = Toast.makeText(context, message, Toast.LENGTH_SHORT) + toast?.show() + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/SearchHistoryRepository.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/SearchHistoryRepository.kt new file mode 100644 index 0000000..015f5d7 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/SearchHistoryRepository.kt @@ -0,0 +1,49 @@ +package com.flamyoad.tsukiviewer.core.repository + +import androidx.lifecycle.LiveData +import androidx.paging.PagedList +import androidx.paging.toLiveData +import com.flamyoad.tsukiviewer.core.db.dao.SearchHistoryDao +import com.flamyoad.tsukiviewer.core.model.SearchHistory +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SearchHistoryRepository @Inject constructor( + val searchHistoryDao: SearchHistoryDao +) { + + fun getAll(pageSize: Int): LiveData> { + return searchHistoryDao.getAll() + .toLiveData(pageSize) + } + + suspend fun insertSearchHistory(item: SearchHistory) { + withContext(Dispatchers.IO) { + val lastInsertedItem = searchHistoryDao.getLatestItem() + + /* 1st condition: If last inserted item is null, means the search history has 0 items + 2nd condition: If user inputs the same thing as previous search, + then there is no need to insert into database + */ + val shouldInsert = lastInsertedItem == null || !lastInsertedItem.sameWith(item) + if (shouldInsert) { + searchHistoryDao.insert(item) + } + } + } + + suspend fun deleteSingle(item: SearchHistory) { + withContext(Dispatchers.IO) { + searchHistoryDao.delete(item) + } + } + + suspend fun deleteAll() { + withContext(Dispatchers.IO) { + searchHistoryDao.deleteAll() + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/TagRepository.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/TagRepository.kt new file mode 100644 index 0000000..7bb118c --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/repository/TagRepository.kt @@ -0,0 +1,21 @@ +package com.flamyoad.tsukiviewer.core.repository + +import androidx.lifecycle.LiveData +import com.flamyoad.tsukiviewer.core.db.dao.TagDao +import com.flamyoad.tsukiviewer.core.model.Tag +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class TagRepository @Inject constructor( + private val tagDao: TagDao +) { + + fun getAll(): LiveData> { + return tagDao.getAll() + } + + fun getAllWithFilter(keyword: String): LiveData> { + return tagDao.getAllWithFilter(keyword) + } +} \ No newline at end of file diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/utils/ImageFileFilter.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/utils/ImageFileFilter.kt new file mode 100644 index 0000000..4193e67 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/utils/ImageFileFilter.kt @@ -0,0 +1,23 @@ +package com.flamyoad.tsukiviewer.core.utils + +import java.io.File +import java.io.FileFilter +import java.util.* + +val imageExtensions = arrayOf("jpg", "png", "gif", "jpeg", "webp", "jpe", "bmp") + +class ImageFileFilter : FileFilter { + + override fun accept(file: File?): Boolean { + if (file == null) { + return false + } + + for (extension in imageExtensions) { + if (file.name.lowercase(Locale.ROOT).endsWith(extension)) { + return true + } + } + return false + } +} diff --git a/core/src/main/java/com/flamyoad/tsukiviewer/core/utils/extensions/FileExtensions.kt b/core/src/main/java/com/flamyoad/tsukiviewer/core/utils/extensions/FileExtensions.kt new file mode 100644 index 0000000..6568be4 --- /dev/null +++ b/core/src/main/java/com/flamyoad/tsukiviewer/core/utils/extensions/FileExtensions.kt @@ -0,0 +1,20 @@ +package com.flamyoad.tsukiviewer.core.utils.extensions + +import androidx.core.net.toUri +import com.flamyoad.tsukiviewer.core.model.Doujin +import com.flamyoad.tsukiviewer.core.utils.ImageFileFilter +import java.io.File + +fun File.toDoujin(): Doujin? { + val imageList = this.listFiles(ImageFileFilter()) ?: return null + if (imageList.isEmpty()) return null + + val doujin = Doujin( + pic = imageList.first().toUri(), + title = this.name, + path = this, + lastModified = this.lastModified(), + numberOfItems = imageList.size + ) + return doujin +}