Implemented Auto Source Migration
Co-Authored-By: NerdNumber9 <nerdnumber9@users.noreply.github.com>
This commit is contained in:
parent
c7dabb9d63
commit
8ba75831e6
37 changed files with 2536 additions and 12 deletions
|
@ -243,6 +243,9 @@ dependencies {
|
||||||
final coroutines_version = '1.3.2'
|
final coroutines_version = '1.3.2'
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||||
|
|
||||||
|
// Text distance
|
||||||
|
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||||
}
|
}
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
|
|
|
@ -1,18 +1,37 @@
|
||||||
package eu.kanade.tachiyomi.data.database
|
package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||||
import eu.kanade.tachiyomi.data.database.mappers.*
|
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
|
||||||
import eu.kanade.tachiyomi.data.database.queries.*
|
import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.SearchMetadataTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.CategoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.ChapterQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.HistoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.MangaQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.SearchMetadataQueries
|
||||||
|
import eu.kanade.tachiyomi.data.database.queries.TrackQueries
|
||||||
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context)
|
open class DatabaseHelper(context: Context)
|
||||||
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries,
|
||||||
|
HistoryQueries, SearchMetadataQueries {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
|
@ -26,6 +45,7 @@ open class DatabaseHelper(context: Context)
|
||||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||||
|
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package eu.kanade.tachiyomi.data.database.mappers
|
||||||
|
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.database.Cursor
|
||||||
|
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_EXTRA
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_EXTRA_VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_INDEXED_EXTRA
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_MANGA_ID
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_UPLOADER
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.TABLE
|
||||||
|
|
||||||
|
class SearchMetadataTypeMapping : SQLiteTypeMapping<SearchMetadata>(
|
||||||
|
SearchMetadataPutResolver(),
|
||||||
|
SearchMetadataGetResolver(),
|
||||||
|
SearchMetadataDeleteResolver()
|
||||||
|
)
|
||||||
|
|
||||||
|
class SearchMetadataPutResolver : DefaultPutResolver<SearchMetadata>() {
|
||||||
|
|
||||||
|
override fun mapToInsertQuery(obj: SearchMetadata) = InsertQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToUpdateQuery(obj: SearchMetadata) = UpdateQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_MANGA_ID = ?")
|
||||||
|
.whereArgs(obj.mangaId)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun mapToContentValues(obj: SearchMetadata) = ContentValues(5).apply {
|
||||||
|
put(COL_MANGA_ID, obj.mangaId)
|
||||||
|
put(COL_UPLOADER, obj.uploader)
|
||||||
|
put(COL_EXTRA, obj.extra)
|
||||||
|
put(COL_INDEXED_EXTRA, obj.indexedExtra)
|
||||||
|
put(COL_EXTRA_VERSION, obj.extraVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchMetadataGetResolver : DefaultGetResolver<SearchMetadata>() {
|
||||||
|
|
||||||
|
override fun mapFromCursor(cursor: Cursor): SearchMetadata = SearchMetadata(
|
||||||
|
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
|
||||||
|
uploader = cursor.getString(cursor.getColumnIndex(COL_UPLOADER)),
|
||||||
|
extra = cursor.getString(cursor.getColumnIndex(COL_EXTRA)),
|
||||||
|
indexedExtra = cursor.getString(cursor.getColumnIndex(COL_INDEXED_EXTRA)),
|
||||||
|
extraVersion = cursor.getInt(cursor.getColumnIndex(COL_EXTRA_VERSION))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchMetadataDeleteResolver : DefaultDeleteResolver<SearchMetadata>() {
|
||||||
|
|
||||||
|
override fun mapToDeleteQuery(obj: SearchMetadata) = DeleteQuery.builder()
|
||||||
|
.table(TABLE)
|
||||||
|
.where("$COL_MANGA_ID = ?")
|
||||||
|
.whereArgs(obj.mangaId)
|
||||||
|
.build()
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package eu.kanade.tachiyomi.data.database.models
|
||||||
|
|
||||||
|
data class SearchMetadata(
|
||||||
|
// Manga ID this gallery is linked to
|
||||||
|
val mangaId: Long,
|
||||||
|
|
||||||
|
// Gallery uploader
|
||||||
|
val uploader: String?,
|
||||||
|
|
||||||
|
// Extra data attached to this metadata, in JSON format
|
||||||
|
val extra: String,
|
||||||
|
|
||||||
|
// Indexed extra data attached to this metadata
|
||||||
|
val indexedExtra: String?,
|
||||||
|
|
||||||
|
// The version of this metadata's extra. Used to track changes to the 'extra' field's schema
|
||||||
|
val extraVersion: Int
|
||||||
|
) {
|
||||||
|
// Transient information attached to this piece of metadata, useful for caching
|
||||||
|
var transientCache: Map<String, Any>? = null
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package eu.kanade.tachiyomi.data.database.queries
|
||||||
|
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.Query
|
||||||
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.SearchMetadata
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable
|
||||||
|
|
||||||
|
interface SearchMetadataQueries : DbProvider {
|
||||||
|
|
||||||
|
fun getSearchMetadataForManga(mangaId: Long) = db.get()
|
||||||
|
.`object`(SearchMetadata::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(SearchMetadataTable.TABLE)
|
||||||
|
.where("${SearchMetadataTable.COL_MANGA_ID} = ?")
|
||||||
|
.whereArgs(mangaId)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getSearchMetadata() = db.get()
|
||||||
|
.listOfObjects(SearchMetadata::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(SearchMetadataTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun getSearchMetadataByIndexedExtra(extra: String) = db.get()
|
||||||
|
.listOfObjects(SearchMetadata::class.java)
|
||||||
|
.withQuery(Query.builder()
|
||||||
|
.table(SearchMetadataTable.TABLE)
|
||||||
|
.where("${SearchMetadataTable.COL_INDEXED_EXTRA} = ?")
|
||||||
|
.whereArgs(extra)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
|
||||||
|
fun insertSearchMetadata(metadata: SearchMetadata) = db.put().`object`(metadata).prepare()
|
||||||
|
|
||||||
|
fun deleteSearchMetadata(metadata: SearchMetadata) = db.delete().`object`(metadata).prepare()
|
||||||
|
|
||||||
|
fun deleteAllSearchMetadata() = db.delete().byQuery(DeleteQuery.builder()
|
||||||
|
.table(SearchMetadataTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.prepare()
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package eu.kanade.tachiyomi.data.database.tables
|
||||||
|
|
||||||
|
|
||||||
|
object SearchMetadataTable {
|
||||||
|
const val TABLE = "search_metadata"
|
||||||
|
|
||||||
|
const val COL_MANGA_ID = "manga_id"
|
||||||
|
|
||||||
|
const val COL_UPLOADER = "uploader"
|
||||||
|
|
||||||
|
const val COL_EXTRA = "extra"
|
||||||
|
|
||||||
|
const val COL_INDEXED_EXTRA = "indexed_extra"
|
||||||
|
|
||||||
|
const val COL_EXTRA_VERSION = "extra_version"
|
||||||
|
|
||||||
|
// Insane foreign, primary key to avoid touch manga table
|
||||||
|
val createTableQuery: String
|
||||||
|
get() = """CREATE TABLE $TABLE(
|
||||||
|
$COL_MANGA_ID INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
$COL_UPLOADER TEXT,
|
||||||
|
$COL_EXTRA TEXT NOT NULL,
|
||||||
|
$COL_INDEXED_EXTRA TEXT,
|
||||||
|
$COL_EXTRA_VERSION INT NOT NULL,
|
||||||
|
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||||
|
ON DELETE CASCADE
|
||||||
|
)"""
|
||||||
|
|
||||||
|
val createUploaderIndexQuery: String
|
||||||
|
get() = "CREATE INDEX ${TABLE}_${COL_UPLOADER}_index ON $TABLE($COL_UPLOADER)"
|
||||||
|
|
||||||
|
val createIndexedExtraIndexQuery: String
|
||||||
|
get() = "CREATE INDEX ${TABLE}_${COL_INDEXED_EXTRA}_index ON $TABLE($COL_INDEXED_EXTRA)"
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package eu.kanade.tachiyomi.smartsearch
|
||||||
|
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.ui.smartsearch.SmartSearchPresenter
|
||||||
|
import eu.kanade.tachiyomi.util.await
|
||||||
|
import info.debatty.java.stringsimilarity.NormalizedLevenshtein
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class SmartSearchEngine(parentContext: CoroutineContext,
|
||||||
|
val extraSearchParams: String? = null): CoroutineScope {
|
||||||
|
override val coroutineContext: CoroutineContext = parentContext + Job() + Dispatchers.Default
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
private val normalizedLevenshtein = NormalizedLevenshtein()
|
||||||
|
|
||||||
|
suspend fun smartSearch(source: CatalogueSource, title: String): SManga? {
|
||||||
|
val cleanedTitle = cleanSmartSearchTitle(title)
|
||||||
|
|
||||||
|
val queries = getSmartSearchQueries(cleanedTitle)
|
||||||
|
|
||||||
|
val eligibleManga = supervisorScope {
|
||||||
|
queries.map { query ->
|
||||||
|
async(Dispatchers.Default) {
|
||||||
|
val builtQuery = if(extraSearchParams != null) {
|
||||||
|
"$query ${extraSearchParams.trim()}"
|
||||||
|
} else query
|
||||||
|
|
||||||
|
val searchResults = source.fetchSearchManga(1, builtQuery, FilterList())
|
||||||
|
.toSingle().await(Schedulers.io())
|
||||||
|
|
||||||
|
searchResults.mangas.map {
|
||||||
|
val cleanedMangaTitle = cleanSmartSearchTitle(it.title)
|
||||||
|
val normalizedDistance = normalizedLevenshtein.similarity(cleanedTitle, cleanedMangaTitle)
|
||||||
|
SmartSearchPresenter.SearchEntry(it, normalizedDistance)
|
||||||
|
}.filter { (_, normalizedDistance) ->
|
||||||
|
normalizedDistance >= MIN_SMART_ELIGIBLE_THRESHOLD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.flatMap { it.await() }
|
||||||
|
}
|
||||||
|
|
||||||
|
return eligibleManga.maxBy { it.dist }?.manga
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun normalSearch(source: CatalogueSource, title: String): SManga? {
|
||||||
|
val eligibleManga = supervisorScope {
|
||||||
|
val searchQuery = if(extraSearchParams != null) {
|
||||||
|
"$title ${extraSearchParams.trim()}"
|
||||||
|
} else title
|
||||||
|
val searchResults = source.fetchSearchManga(1, searchQuery, FilterList()).toSingle().await(Schedulers.io())
|
||||||
|
|
||||||
|
searchResults.mangas.map {
|
||||||
|
val normalizedDistance = normalizedLevenshtein.similarity(title, it.title)
|
||||||
|
SmartSearchPresenter.SearchEntry(it, normalizedDistance)
|
||||||
|
}.filter { (_, normalizedDistance) ->
|
||||||
|
normalizedDistance >= MIN_NORMAL_ELIGIBLE_THRESHOLD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eligibleManga.maxBy { it.dist }?.manga
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSmartSearchQueries(cleanedTitle: String): List<String> {
|
||||||
|
val splitCleanedTitle = cleanedTitle.split(" ")
|
||||||
|
val splitSortedByLargest = splitCleanedTitle.sortedByDescending { it.length }
|
||||||
|
|
||||||
|
if(splitCleanedTitle.isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search cleaned title
|
||||||
|
// Search two largest words
|
||||||
|
// Search largest word
|
||||||
|
// Search first two words
|
||||||
|
// Search first word
|
||||||
|
|
||||||
|
val searchQueries = listOf(
|
||||||
|
listOf(cleanedTitle),
|
||||||
|
splitSortedByLargest.take(2),
|
||||||
|
splitSortedByLargest.take(1),
|
||||||
|
splitCleanedTitle.take(2),
|
||||||
|
splitCleanedTitle.take(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
return searchQueries.map {
|
||||||
|
it.joinToString(" ").trim()
|
||||||
|
}.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanSmartSearchTitle(title: String): String {
|
||||||
|
val preTitle = title.toLowerCase()
|
||||||
|
|
||||||
|
// Remove text in brackets
|
||||||
|
var cleanedTitle = removeTextInBrackets(preTitle, true)
|
||||||
|
if(cleanedTitle.length <= 5) { // Title is suspiciously short, try parsing it backwards
|
||||||
|
cleanedTitle = removeTextInBrackets(preTitle, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip non-special characters
|
||||||
|
cleanedTitle = cleanedTitle.replace(titleRegex, " ")
|
||||||
|
|
||||||
|
// Strip splitters and consecutive spaces
|
||||||
|
cleanedTitle = cleanedTitle.trim().replace(" - ", " ").replace(consecutiveSpacesRegex, " ").trim()
|
||||||
|
|
||||||
|
return cleanedTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeTextInBrackets(text: String, readForward: Boolean): String {
|
||||||
|
val bracketPairs = listOf(
|
||||||
|
'(' to ')',
|
||||||
|
'[' to ']',
|
||||||
|
'<' to '>',
|
||||||
|
'{' to '}'
|
||||||
|
)
|
||||||
|
var openingBracketPairs = bracketPairs.mapIndexed { index, (opening, _) ->
|
||||||
|
opening to index
|
||||||
|
}.toMap()
|
||||||
|
var closingBracketPairs = bracketPairs.mapIndexed { index, (_, closing) ->
|
||||||
|
closing to index
|
||||||
|
}.toMap()
|
||||||
|
|
||||||
|
// Reverse pairs if reading backwards
|
||||||
|
if(!readForward) {
|
||||||
|
val tmp = openingBracketPairs
|
||||||
|
openingBracketPairs = closingBracketPairs
|
||||||
|
closingBracketPairs = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
val depthPairs = bracketPairs.map { 0 }.toMutableList()
|
||||||
|
|
||||||
|
val result = StringBuilder()
|
||||||
|
for(c in if(readForward) text else text.reversed()) {
|
||||||
|
val openingBracketDepthIndex = openingBracketPairs[c]
|
||||||
|
if(openingBracketDepthIndex != null) {
|
||||||
|
depthPairs[openingBracketDepthIndex]++
|
||||||
|
} else {
|
||||||
|
val closingBracketDepthIndex = closingBracketPairs[c]
|
||||||
|
if(closingBracketDepthIndex != null) {
|
||||||
|
depthPairs[closingBracketDepthIndex]--
|
||||||
|
} else {
|
||||||
|
if(depthPairs.all { it <= 0 }) {
|
||||||
|
result.append(c)
|
||||||
|
} else {
|
||||||
|
// In brackets, do not append to result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||||
|
* if the manga is not yet in the database.
|
||||||
|
*
|
||||||
|
* @param sManga the manga from the source.
|
||||||
|
* @return a manga from the database.
|
||||||
|
*/
|
||||||
|
suspend fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||||
|
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||||
|
if (localManga == null) {
|
||||||
|
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||||
|
newManga.copyFrom(sManga)
|
||||||
|
val result = db.insertManga(newManga).executeAsBlocking()
|
||||||
|
newManga.id = result.insertedId()
|
||||||
|
localManga = newManga
|
||||||
|
}
|
||||||
|
return localManga
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MIN_SMART_ELIGIBLE_THRESHOLD = 0.4
|
||||||
|
const val MIN_NORMAL_ELIGIBLE_THRESHOLD = 0.4
|
||||||
|
|
||||||
|
private val titleRegex = Regex("[^a-zA-Z0-9- ]")
|
||||||
|
private val consecutiveSpacesRegex = Regex(" +")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.ui.catalogue
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuInflater
|
import android.view.MenuInflater
|
||||||
|
@ -28,6 +29,7 @@ import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
||||||
import eu.kanade.tachiyomi.util.RecyclerWindowInsetsListener
|
import eu.kanade.tachiyomi.util.RecyclerWindowInsetsListener
|
||||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
@ -239,4 +241,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
|
class SettingsSourcesFadeChangeHandler : FadeChangeHandler()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class SmartSearchConfig(val origTitle: String, val origMangaId: Long) : Parcelable
|
||||||
}
|
}
|
|
@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.library.HeightTopWindowInsetsListener
|
import eu.kanade.tachiyomi.ui.library.HeightTopWindowInsetsListener
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
@ -68,6 +69,18 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||||
FlexibleAdapter.EndlessScrollListener,
|
FlexibleAdapter.EndlessScrollListener,
|
||||||
ChangeMangaCategoriesDialog.Listener {
|
ChangeMangaCategoriesDialog.Listener {
|
||||||
|
|
||||||
|
constructor(source: CatalogueSource,
|
||||||
|
searchQuery: String? = null,
|
||||||
|
smartSearchConfig: CatalogueController.SmartSearchConfig? = null) : this(Bundle().apply {
|
||||||
|
putLong(SOURCE_ID_KEY, source.id)
|
||||||
|
|
||||||
|
if(searchQuery != null)
|
||||||
|
putString(SEARCH_QUERY_KEY, searchQuery)
|
||||||
|
|
||||||
|
if (smartSearchConfig != null)
|
||||||
|
putParcelable(SMART_SEARCH_CONFIG_KEY, smartSearchConfig)
|
||||||
|
})
|
||||||
|
|
||||||
constructor(source: CatalogueSource) : this(Bundle().apply {
|
constructor(source: CatalogueSource) : this(Bundle().apply {
|
||||||
putLong(SOURCE_ID_KEY, source.id)
|
putLong(SOURCE_ID_KEY, source.id)
|
||||||
})
|
})
|
||||||
|
@ -579,6 +592,9 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||||
|
|
||||||
protected companion object {
|
protected companion object {
|
||||||
const val SOURCE_ID_KEY = "sourceId"
|
const val SOURCE_ID_KEY = "sourceId"
|
||||||
|
|
||||||
|
const val SEARCH_QUERY_KEY = "searchQuery"
|
||||||
|
const val SMART_SEARCH_CONFIG_KEY = "smartSearchConfig"
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
||||||
import eu.kanade.tachiyomi.ui.migration.MigrationInterface
|
import eu.kanade.tachiyomi.ui.migration.MigrationInterface
|
||||||
import eu.kanade.tachiyomi.ui.migration.SearchController
|
import eu.kanade.tachiyomi.ui.migration.SearchController
|
||||||
|
import eu.kanade.tachiyomi.ui.migration.manga.design.MigrationDesignController
|
||||||
import eu.kanade.tachiyomi.util.doOnApplyWindowInsets
|
import eu.kanade.tachiyomi.util.doOnApplyWindowInsets
|
||||||
import eu.kanade.tachiyomi.util.inflate
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
import eu.kanade.tachiyomi.util.marginBottom
|
import eu.kanade.tachiyomi.util.marginBottom
|
||||||
|
@ -467,7 +468,13 @@ class LibraryController(
|
||||||
selectAllRelay.call(it)
|
selectAllRelay.call(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
R.id.action_migrate -> startMangaMigration()
|
R.id.action_migrate -> {
|
||||||
|
router.pushController(
|
||||||
|
MigrationDesignController.create(
|
||||||
|
selectedMangas.mapNotNull { it.id }
|
||||||
|
).withFadeTransaction())
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
} //startMangaMigration()
|
||||||
R.id.action_hide_title -> {
|
R.id.action_hide_title -> {
|
||||||
val showAll = (selectedMangas.filter { (it as? LibraryManga)?.hide_title == true }
|
val showAll = (selectedMangas.filter { (it as? LibraryManga)?.hide_title == true }
|
||||||
).size == selectedMangas.size
|
).size == selectedMangas.size
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package eu.kanade.tachiyomi.ui.manga
|
package eu.kanade.tachiyomi.ui.manga
|
||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -27,6 +26,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
||||||
|
@ -40,6 +40,21 @@ import java.util.Date
|
||||||
|
|
||||||
class MangaController : RxController, TabbedController {
|
class MangaController : RxController, TabbedController {
|
||||||
|
|
||||||
|
constructor(manga: Manga?,
|
||||||
|
fromCatalogue: Boolean = false,
|
||||||
|
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
|
||||||
|
update: Boolean = false) : super(Bundle().apply {
|
||||||
|
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
||||||
|
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
||||||
|
putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
||||||
|
putBoolean(UPDATE_EXTRA, update)
|
||||||
|
}) {
|
||||||
|
this.manga = manga
|
||||||
|
if (manga != null) {
|
||||||
|
source = Injekt.get<SourceManager>().getOrStub(manga.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(manga: Manga?, fromCatalogue: Boolean = false, fromExtension: Boolean = false) :
|
constructor(manga: Manga?, fromCatalogue: Boolean = false, fromExtension: Boolean = false) :
|
||||||
super
|
super
|
||||||
(Bundle()
|
(Bundle()
|
||||||
|
@ -213,6 +228,10 @@ class MangaController : RxController, TabbedController {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val UPDATE_EXTRA = "update"
|
||||||
|
const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
|
||||||
|
|
||||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||||
const val MANGA_EXTRA = "manga"
|
const val MANGA_EXTRA = "manga"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.text.Html
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
class MetadataFetchDialog {
|
||||||
|
|
||||||
|
val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
val preferenceHelper: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
fun show(context: Activity) {
|
||||||
|
//Too lazy to actually deal with orientation changes
|
||||||
|
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||||
|
|
||||||
|
var running = true
|
||||||
|
|
||||||
|
val progressDialog = MaterialDialog.Builder(context)
|
||||||
|
.title("Fetching library metadata")
|
||||||
|
.content("Preparing library")
|
||||||
|
.progress(false, 0, true)
|
||||||
|
.negativeText("Stop")
|
||||||
|
.onNegative { dialog, which ->
|
||||||
|
running = false
|
||||||
|
dialog.dismiss()
|
||||||
|
notifyMigrationStopped(context)
|
||||||
|
}
|
||||||
|
.cancelable(false)
|
||||||
|
.canceledOnTouchOutside(false)
|
||||||
|
.show()
|
||||||
|
|
||||||
|
thread {
|
||||||
|
val libraryMangas = db.getLibraryMangas().executeAsBlocking()
|
||||||
|
//.filter { isLewdSource(it.source) }
|
||||||
|
.distinctBy { it.id }
|
||||||
|
|
||||||
|
context.runOnUiThread {
|
||||||
|
progressDialog.maxProgress = libraryMangas.size
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaWithMissingMetadata = libraryMangas
|
||||||
|
.filterIndexed { index, libraryManga ->
|
||||||
|
if(index % 100 == 0) {
|
||||||
|
context.runOnUiThread {
|
||||||
|
progressDialog.setContent("[Stage 1/2] Scanning for missing metadata...")
|
||||||
|
progressDialog.setProgress(index + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.getSearchMetadataForManga(libraryManga.id!!).executeAsBlocking() == null
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
context.runOnUiThread {
|
||||||
|
progressDialog.maxProgress = mangaWithMissingMetadata.size
|
||||||
|
}
|
||||||
|
|
||||||
|
//Actual metadata fetch code
|
||||||
|
for((i, manga) in mangaWithMissingMetadata.withIndex()) {
|
||||||
|
if(!running) break
|
||||||
|
context.runOnUiThread {
|
||||||
|
progressDialog.setContent("[Stage 2/2] Processing: ${manga.title}")
|
||||||
|
progressDialog.setProgress(i + 1)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
val source = sourceManager.get(manga.source)
|
||||||
|
source?.let {
|
||||||
|
manga.copyFrom(it.fetchMangaDetails(manga).toBlocking().first())
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Timber.e(t, "Could not migrate manga!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.runOnUiThread {
|
||||||
|
// Ensure activity still exists before we do anything to the activity
|
||||||
|
if(!context.isDestroyed) {
|
||||||
|
progressDialog.dismiss()
|
||||||
|
|
||||||
|
//Enable orientation changes again
|
||||||
|
context.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
|
||||||
|
|
||||||
|
if (running) displayMigrationComplete(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun askMigration(activity: Activity, explicit: Boolean) {
|
||||||
|
var extra = ""
|
||||||
|
db.getLibraryMangas().asRxSingle().subscribe {
|
||||||
|
/*if(!explicit && it.none { isLewdSource(it.source) }) {
|
||||||
|
// Do not open dialog on startup if no manga
|
||||||
|
// Also do not check again
|
||||||
|
preferenceHelper.migrateLibraryAsked().set(true)
|
||||||
|
} else {*/
|
||||||
|
activity.runOnUiThread {
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
|
.title("Fetch library metadata")
|
||||||
|
.content(Html.fromHtml("You need to fetch your library's metadata before tag searching in the library will function.<br><br>" +
|
||||||
|
"This process may take a long time depending on your library size and will also use up a significant amount of internet bandwidth but can be stopped and started whenever you wish.<br><br>" +
|
||||||
|
extra +
|
||||||
|
"This process can be done later if required."))
|
||||||
|
.positiveText("Migrate")
|
||||||
|
.negativeText("Later")
|
||||||
|
.onPositive { _, _ -> show(activity) }
|
||||||
|
.onNegative { _, _ -> adviseMigrationLater(activity) }
|
||||||
|
//.onAny { _, _ -> preferenceHelper.migrateLibraryAsked().set(true) }
|
||||||
|
.cancelable(false)
|
||||||
|
.canceledOnTouchOutside(false)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun adviseMigrationLater(activity: Activity) {
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
|
.title("Metadata fetch canceled")
|
||||||
|
.content("Library metadata fetch has been canceled.\n\n" +
|
||||||
|
"You can run this operation later by going to: Settings > Advanced > Migrate library metadata")
|
||||||
|
.positiveText("Ok")
|
||||||
|
.cancelable(true)
|
||||||
|
.canceledOnTouchOutside(true)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun notifyMigrationStopped(activity: Activity) {
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
|
.title("Metadata fetch stopped")
|
||||||
|
.content("Library metadata fetch has been stopped.\n\n" +
|
||||||
|
"You can continue this operation later by going to: Settings > Advanced > Migrate library metadata")
|
||||||
|
.positiveText("Ok")
|
||||||
|
.cancelable(true)
|
||||||
|
.canceledOnTouchOutside(true)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayMigrationComplete(activity: Activity) {
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
|
.title("Migration complete")
|
||||||
|
.content("${activity.getString(R.string.app_name)} is now ready for use!")
|
||||||
|
.positiveText("Ok")
|
||||||
|
.cancelable(true)
|
||||||
|
.canceledOnTouchOutside(true)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,17 +9,27 @@ import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.migration.manga.design.MigrationDesignController
|
||||||
import eu.kanade.tachiyomi.util.RecyclerWindowInsetsListener
|
import eu.kanade.tachiyomi.util.RecyclerWindowInsetsListener
|
||||||
|
import eu.kanade.tachiyomi.util.await
|
||||||
|
import eu.kanade.tachiyomi.util.launchUI
|
||||||
import kotlinx.android.synthetic.main.migration_controller.*
|
import kotlinx.android.synthetic.main.migration_controller.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class MigrationController : NucleusController<MigrationPresenter>(),
|
class MigrationController : NucleusController<MigrationPresenter>(),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
SourceAdapter.OnSelectClickListener,
|
SourceAdapter.OnSelectClickListener,
|
||||||
|
SourceAdapter.OnAutoClickListener,
|
||||||
MigrationInterface {
|
MigrationInterface {
|
||||||
|
|
||||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
@ -119,6 +129,19 @@ class MigrationController : NucleusController<MigrationPresenter>(),
|
||||||
onItemClick(view, position)
|
onItemClick(view, position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onAutoClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) as? SourceItem ?: return
|
||||||
|
|
||||||
|
launchUI {
|
||||||
|
val manga = Injekt.get<DatabaseHelper>().getFavoriteMangas().asRxSingle().await(
|
||||||
|
Schedulers.io())
|
||||||
|
val sourceMangas = manga.asSequence().filter { it.source == item.source.id }.map { it.id!! }.toList()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
router.pushController(MigrationDesignController.create(sourceMangas).withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean): Manga? {
|
override fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean): Manga? {
|
||||||
presenter.migrateManga(prevManga, manga, replace)
|
presenter.migrateManga(prevManga, manga, replace)
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
object MigrationFlags {
|
object MigrationFlags {
|
||||||
|
|
||||||
private const val CHAPTERS = 0b001
|
const val CHAPTERS = 0b001
|
||||||
private const val CATEGORIES = 0b010
|
const val CATEGORIES = 0b010
|
||||||
private const val TRACK = 0b100
|
const val TRACK = 0b100
|
||||||
|
|
||||||
private const val CHAPTERS2 = 0x1
|
private const val CHAPTERS2 = 0x1
|
||||||
private const val CATEGORIES2 = 0x2
|
private const val CATEGORIES2 = 0x2
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration
|
||||||
|
|
||||||
|
class MigrationStatus {
|
||||||
|
companion object {
|
||||||
|
val NOT_INITIALIZED = -1
|
||||||
|
val COMPLETED = 0
|
||||||
|
|
||||||
|
//Migration process
|
||||||
|
val NOTIFY_USER = 1
|
||||||
|
val OPEN_BACKUP_MENU = 2
|
||||||
|
val PERFORM_BACKUP = 3
|
||||||
|
val FINALIZE_MIGRATION = 4
|
||||||
|
|
||||||
|
val MAX_MIGRATION_STEPS = 2
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,18 @@ class SourceAdapter(val controller: MigrationController) :
|
||||||
fun onSelectClick(position: Int)
|
fun onSelectClick(position: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener for auto item clicks.
|
||||||
|
*/
|
||||||
|
val autoClickListener: OnAutoClickListener? = controller
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listener which should be called when user clicks select.
|
||||||
|
*/
|
||||||
|
interface OnAutoClickListener {
|
||||||
|
fun onAutoClick(position: Int)
|
||||||
|
}
|
||||||
|
|
||||||
override fun updateDataSet(items: MutableList<IFlexible<*>>?) {
|
override fun updateDataSet(items: MutableList<IFlexible<*>>?) {
|
||||||
if (this.items !== items) {
|
if (this.items !== items) {
|
||||||
this.items = items
|
this.items = items
|
||||||
|
|
|
@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
|
||||||
import eu.kanade.tachiyomi.util.getRound
|
import eu.kanade.tachiyomi.util.getRound
|
||||||
import eu.kanade.tachiyomi.util.gone
|
|
||||||
import io.github.mthli.slice.Slice
|
import io.github.mthli.slice.Slice
|
||||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
|
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
|
||||||
|
|
||||||
|
@ -21,11 +20,14 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||||
get() = card
|
get() = card
|
||||||
|
|
||||||
init {
|
init {
|
||||||
source_latest.gone()
|
source_latest.text = "Auto"
|
||||||
source_browse.setText(R.string.select)
|
source_browse.setText(R.string.select)
|
||||||
source_browse.setOnClickListener {
|
source_browse.setOnClickListener {
|
||||||
adapter.selectClickListener?.onSelectClick(adapterPosition)
|
adapter.selectClickListener?.onSelectClick(adapterPosition)
|
||||||
}
|
}
|
||||||
|
source_latest.setOnClickListener {
|
||||||
|
adapter.autoClickListener?.onAutoClick(adapterPosition)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(item: SourceItem) {
|
fun bind(item: SourceItem) {
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration.manga.design
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
|
||||||
|
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureController
|
||||||
|
import eu.kanade.tachiyomi.util.gone
|
||||||
|
import eu.kanade.tachiyomi.util.visible
|
||||||
|
import exh.ui.migration.manga.process.MigrationProcedureConfig
|
||||||
|
import kotlinx.android.synthetic.main.migration_design_controller.*
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class MigrationDesignController(bundle: Bundle? = null) : BaseController(bundle), FlexibleAdapter
|
||||||
|
.OnItemClickListener {
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
private val prefs: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
private var adapter: MigrationSourceAdapter? = null
|
||||||
|
|
||||||
|
private val config: LongArray = args.getLongArray(MANGA_IDS_EXTRA) ?: LongArray(0)
|
||||||
|
|
||||||
|
private var showingOptions = false
|
||||||
|
|
||||||
|
override fun getTitle() = "Select target sources"
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
return inflater.inflate(R.layout.migration_design_controller, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
val ourAdapter = adapter ?: MigrationSourceAdapter(
|
||||||
|
getEnabledSources().map { MigrationSourceItem(it, true) },
|
||||||
|
this
|
||||||
|
)
|
||||||
|
adapter = ourAdapter
|
||||||
|
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||||
|
recycler.setHasFixedSize(true)
|
||||||
|
recycler.adapter = ourAdapter
|
||||||
|
ourAdapter.itemTouchHelperCallback = null // Reset adapter touch adapter to fix drag after rotation
|
||||||
|
ourAdapter.isHandleDragEnabled = true
|
||||||
|
|
||||||
|
migration_mode.setOnClickListener {
|
||||||
|
prioritize_chapter_count.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
fuzzy_search.setOnClickListener {
|
||||||
|
use_smart_search.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_manga_desc.setOnClickListener {
|
||||||
|
copy_manga.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
extra_search_param_desc.setOnClickListener {
|
||||||
|
extra_search_param.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
prioritize_chapter_count.setOnCheckedChangeListener { _, b ->
|
||||||
|
updatePrioritizeChapterCount(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
extra_search_param.setOnCheckedChangeListener { _, b ->
|
||||||
|
updateOptionsState()
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePrioritizeChapterCount(prioritize_chapter_count.isChecked)
|
||||||
|
|
||||||
|
updateOptionsState()
|
||||||
|
|
||||||
|
begin_migration_btn.setOnClickListener {
|
||||||
|
if(!showingOptions) {
|
||||||
|
showingOptions = true
|
||||||
|
updateOptionsState()
|
||||||
|
return@setOnClickListener
|
||||||
|
}
|
||||||
|
|
||||||
|
var flags = 0
|
||||||
|
if(mig_chapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
|
||||||
|
if(mig_categories.isChecked) flags = flags or MigrationFlags.CATEGORIES
|
||||||
|
if(mig_categories.isChecked) flags = flags or MigrationFlags.TRACK
|
||||||
|
|
||||||
|
router.replaceTopController(
|
||||||
|
MigrationProcedureController.create(
|
||||||
|
MigrationProcedureConfig(
|
||||||
|
config.toList(),
|
||||||
|
ourAdapter.items.filter {
|
||||||
|
it.sourceEnabled
|
||||||
|
}.map { it.source.id },
|
||||||
|
useSourceWithMostChapters = prioritize_chapter_count.isChecked,
|
||||||
|
enableLenientSearch = use_smart_search.isChecked,
|
||||||
|
migrationFlags = flags,
|
||||||
|
copy = copy_manga.isChecked,
|
||||||
|
extraSearchParams = if(extra_search_param.isChecked && extra_search_param_text.text.isNotBlank()) {
|
||||||
|
extra_search_param_text.text.toString()
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
).withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateOptionsState() {
|
||||||
|
if (showingOptions) {
|
||||||
|
begin_migration_btn.text = "Begin migration"
|
||||||
|
options_group.visible()
|
||||||
|
if(extra_search_param.isChecked) {
|
||||||
|
extra_search_param_text.visible()
|
||||||
|
} else {
|
||||||
|
extra_search_param_text.gone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
begin_migration_btn.text = "Next step"
|
||||||
|
options_group.gone()
|
||||||
|
extra_search_param_text.gone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleBack(): Boolean {
|
||||||
|
if(showingOptions) {
|
||||||
|
showingOptions = false
|
||||||
|
updateOptionsState()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.handleBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
adapter?.onSaveInstanceState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Still incorrect, why is this called before onViewCreated?
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
adapter?.onRestoreInstanceState(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePrioritizeChapterCount(migrationMode: Boolean) {
|
||||||
|
migration_mode.text = if(migrationMode) {
|
||||||
|
"Currently using the source with the most chapters and the above list to break ties (slow with many sources or smart search)"
|
||||||
|
} else {
|
||||||
|
"Currently using the first source in the list that has the manga"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(view: View, position: Int): Boolean {
|
||||||
|
adapter?.getItem(position)?.let {
|
||||||
|
it.sourceEnabled = !it.sourceEnabled
|
||||||
|
}
|
||||||
|
adapter?.notifyItemChanged(position)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of enabled sources ordered by language and name.
|
||||||
|
*
|
||||||
|
* @return list containing enabled sources.
|
||||||
|
*/
|
||||||
|
private fun getEnabledSources(): List<HttpSource> {
|
||||||
|
val languages = prefs.enabledLanguages().getOrDefault()
|
||||||
|
val hiddenCatalogues = prefs.hiddenCatalogues().getOrDefault()
|
||||||
|
|
||||||
|
return sourceManager.getCatalogueSources()
|
||||||
|
.filterIsInstance<HttpSource>()
|
||||||
|
.filter { it.lang in languages }
|
||||||
|
.filterNot { it.id.toString() in hiddenCatalogues }
|
||||||
|
.sortedBy { "(${it.lang}) ${it.name}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MANGA_IDS_EXTRA = "manga_ids"
|
||||||
|
|
||||||
|
fun create(mangaIds: List<Long>): MigrationDesignController {
|
||||||
|
return MigrationDesignController(Bundle().apply {
|
||||||
|
putLongArray(MANGA_IDS_EXTRA, mangaIds.toLongArray())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration.manga.design
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class MigrationSourceAdapter(val items: List<MigrationSourceItem>,
|
||||||
|
val controller: MigrationDesignController
|
||||||
|
): FlexibleAdapter<MigrationSourceItem>(
|
||||||
|
items,
|
||||||
|
controller,
|
||||||
|
true
|
||||||
|
) {
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
|
||||||
|
outState.putParcelableArrayList(SELECTED_SOURCES_KEY, ArrayList(currentItems.map {
|
||||||
|
it.asParcelable()
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
|
val sourceManager:SourceManager by injectLazy()
|
||||||
|
savedInstanceState.getParcelableArrayList<MigrationSourceItem.ParcelableSI>(
|
||||||
|
SELECTED_SOURCES_KEY
|
||||||
|
)?.let {
|
||||||
|
updateDataSet(it.map { MigrationSourceItem.fromParcelable(sourceManager, it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val SELECTED_SOURCES_KEY = "selected_sources"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration.manga.design
|
||||||
|
|
||||||
|
import android.graphics.Paint.STRIKE_THRU_TEXT_FLAG
|
||||||
|
import android.view.View
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
|
import eu.kanade.tachiyomi.util.getRound
|
||||||
|
import kotlinx.android.synthetic.main.migration_source_item.*
|
||||||
|
|
||||||
|
class MigrationSourceHolder(view: View, val adapter: FlexibleAdapter<MigrationSourceItem>):
|
||||||
|
BaseFlexibleViewHolder(view, adapter) {
|
||||||
|
init {
|
||||||
|
setDragHandleView(reorder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bind(source: HttpSource, sourceEnabled: Boolean) {
|
||||||
|
// Set capitalized title.
|
||||||
|
title.text = source.name.capitalize()
|
||||||
|
|
||||||
|
// Update circle letter image.
|
||||||
|
itemView.post {
|
||||||
|
image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false))
|
||||||
|
}
|
||||||
|
|
||||||
|
if(sourceEnabled) {
|
||||||
|
title.alpha = 1.0f
|
||||||
|
image.alpha = 1.0f
|
||||||
|
title.paintFlags = title.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
|
||||||
|
} else {
|
||||||
|
title.alpha = DISABLED_ALPHA
|
||||||
|
image.alpha = DISABLED_ALPHA
|
||||||
|
title.paintFlags = title.paintFlags or STRIKE_THRU_TEXT_FLAG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DISABLED_ALPHA = 0.3f
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration.manga.design
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
class MigrationSourceItem(val source: HttpSource, var sourceEnabled: Boolean): AbstractFlexibleItem<MigrationSourceHolder>() {
|
||||||
|
override fun getLayoutRes() = R.layout.migration_source_item
|
||||||
|
|
||||||
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MigrationSourceHolder {
|
||||||
|
return MigrationSourceHolder(view, adapter as MigrationSourceAdapter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the given view holder with this item.
|
||||||
|
*
|
||||||
|
* @param adapter The adapter of this item.
|
||||||
|
* @param holder The holder to bind.
|
||||||
|
* @param position The position of this item in the adapter.
|
||||||
|
* @param payloads List of partial changes.
|
||||||
|
*/
|
||||||
|
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||||
|
holder: MigrationSourceHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: List<Any?>?) {
|
||||||
|
holder.bind(source, sourceEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if this item is draggable.
|
||||||
|
*/
|
||||||
|
override fun isDraggable(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other is MigrationSourceItem) {
|
||||||
|
return source.id == other.source.id
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return source.id.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class ParcelableSI(val sourceId: Long, val sourceEnabled: Boolean): Parcelable
|
||||||
|
|
||||||
|
fun asParcelable(): ParcelableSI {
|
||||||
|
return ParcelableSI(source.id, sourceEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromParcelable(sourceManager: SourceManager, si: ParcelableSI): MigrationSourceItem? {
|
||||||
|
val source = sourceManager.get(si.sourceId) as? HttpSource ?: return null
|
||||||
|
|
||||||
|
return MigrationSourceItem(
|
||||||
|
source,
|
||||||
|
si.sourceEnabled
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration.manga.process
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.MotionEvent
|
||||||
|
import androidx.viewpager.widget.ViewPager
|
||||||
|
|
||||||
|
class DeactivatableViewPager: ViewPager {
|
||||||
|
constructor(context: Context): super(context)
|
||||||
|
constructor(context: Context, attrs: AttributeSet): super(context, attrs)
|
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
return !isEnabled || super.onTouchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
|
||||||
|
return isEnabled && super.onInterceptTouchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package exh.ui.migration.manga.process
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.util.DeferredField
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.channels.ConflatedBroadcastChannel
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class MigratingManga(private val db: DatabaseHelper,
|
||||||
|
private val sourceManager: SourceManager,
|
||||||
|
val mangaId: Long,
|
||||||
|
parentContext: CoroutineContext) {
|
||||||
|
val searchResult = DeferredField<Long?>()
|
||||||
|
|
||||||
|
// <MAX, PROGRESS>
|
||||||
|
val progress = ConflatedBroadcastChannel(1 to 0)
|
||||||
|
|
||||||
|
val migrationJob = parentContext + SupervisorJob() + Dispatchers.Default
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var manga: Manga? = null
|
||||||
|
suspend fun manga(): Manga? {
|
||||||
|
if(manga == null) manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
|
return manga
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun mangaSource(): Source {
|
||||||
|
return sourceManager.getOrStub(manga()?.source ?: -1)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,281 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration.manga.process
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.viewpager.widget.PagerAdapter
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
|
||||||
|
import eu.kanade.tachiyomi.util.gone
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import eu.kanade.tachiyomi.util.visible
|
||||||
|
import exh.ui.migration.manga.process.MigratingManga
|
||||||
|
import kotlinx.android.synthetic.main.migration_manga_card.view.*
|
||||||
|
import kotlinx.android.synthetic.main.migration_process_item.view.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.flow.asFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
class MigrationProcedureAdapter(val controller: MigrationProcedureController,
|
||||||
|
val migratingManga: List<MigratingManga>,
|
||||||
|
override val coroutineContext: CoroutineContext) : PagerAdapter(), CoroutineScope {
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
private val gson: Gson by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
override fun isViewFromObject(p0: View, p1: Any): Boolean {
|
||||||
|
return p0 == p1
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCount() = migratingManga.size
|
||||||
|
|
||||||
|
override fun instantiateItem(container: ViewGroup, position: Int): Any {
|
||||||
|
val item = migratingManga[position]
|
||||||
|
val view = container.inflate(R.layout.migration_process_item)
|
||||||
|
container.addView(view)
|
||||||
|
|
||||||
|
view.skip_migration.setOnClickListener {
|
||||||
|
controller.nextMigration()
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewTag = ViewTag(coroutineContext)
|
||||||
|
view.tag = viewTag
|
||||||
|
view.setupView(viewTag, item)
|
||||||
|
|
||||||
|
view.accept_migration.setOnClickListener {
|
||||||
|
viewTag.launch(Dispatchers.Main) {
|
||||||
|
view.migrating_frame.visible()
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
performMigration(item)
|
||||||
|
}
|
||||||
|
controller.nextMigration()
|
||||||
|
} catch(e: Exception) {
|
||||||
|
controller.migrationFailure()
|
||||||
|
}
|
||||||
|
view.migrating_frame.gone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun performMigration(manga: MigratingManga) {
|
||||||
|
if(!manga.searchResult.initialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val toMangaObj = db.getManga(manga.searchResult.get() ?: return).executeAsBlocking() ?: return
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
migrateMangaInternal(
|
||||||
|
manga.manga() ?: return@withContext,
|
||||||
|
toMangaObj,
|
||||||
|
!(controller.config?.copy ?: false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateMangaInternal(prevManga: Manga,
|
||||||
|
manga: Manga,
|
||||||
|
replace: Boolean) {
|
||||||
|
val config = controller.config ?: return
|
||||||
|
db.inTransaction {
|
||||||
|
// Update chapters read
|
||||||
|
if (MigrationFlags.hasChapters(controller.config.migrationFlags)) {
|
||||||
|
val prevMangaChapters = db.getChapters(prevManga).executeAsBlocking()
|
||||||
|
val maxChapterRead = prevMangaChapters.filter { it.read }
|
||||||
|
.maxBy { it.chapter_number }?.chapter_number
|
||||||
|
if (maxChapterRead != null) {
|
||||||
|
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
for (chapter in dbChapters) {
|
||||||
|
if (chapter.isRecognizedNumber && chapter.chapter_number <= maxChapterRead) {
|
||||||
|
chapter.read = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.insertChapters(dbChapters).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update categories
|
||||||
|
if (MigrationFlags.hasCategories(controller.config.migrationFlags)) {
|
||||||
|
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
|
||||||
|
val mangaCategories = categories.map { MangaCategory.create(manga, it) }
|
||||||
|
db.setMangaCategories(mangaCategories, listOf(manga))
|
||||||
|
}
|
||||||
|
// Update track
|
||||||
|
if (MigrationFlags.hasTracks(controller.config.migrationFlags)) {
|
||||||
|
val tracks = db.getTracks(prevManga).executeAsBlocking()
|
||||||
|
for (track in tracks) {
|
||||||
|
track.id = null
|
||||||
|
track.manga_id = manga.id!!
|
||||||
|
}
|
||||||
|
db.insertTracks(tracks).executeAsBlocking()
|
||||||
|
}
|
||||||
|
// Update favorite status
|
||||||
|
if (replace) {
|
||||||
|
prevManga.favorite = false
|
||||||
|
db.updateMangaFavorite(prevManga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
manga.favorite = true
|
||||||
|
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||||
|
|
||||||
|
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
|
||||||
|
db.updateMangaTitle(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun View.setupView(tag: ViewTag, migratingManga: MigratingManga) {
|
||||||
|
tag.launch {
|
||||||
|
val manga = migratingManga.manga()
|
||||||
|
val source = migratingManga.mangaSource()
|
||||||
|
if(manga != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
migration_manga_card_from.loading_group.gone()
|
||||||
|
migration_manga_card_from.attachManga(tag, manga, source)
|
||||||
|
migration_manga_card_from.setOnClickListener {
|
||||||
|
controller.router.pushController(MangaController(manga, true).withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tag.launch {
|
||||||
|
migratingManga.progress.asFlow().collect { (max, progress) ->
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
migration_manga_card_to.search_progress.let { progressBar ->
|
||||||
|
progressBar.max = max
|
||||||
|
progressBar.progress = progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchResult = migratingManga.searchResult.get()?.let {
|
||||||
|
db.getManga(it).executeAsBlocking()
|
||||||
|
}
|
||||||
|
val resultSource = searchResult?.source?.let {
|
||||||
|
sourceManager.get(it)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if(searchResult != null && resultSource != null) {
|
||||||
|
migration_manga_card_to.loading_group.gone()
|
||||||
|
migration_manga_card_to.attachManga(tag, searchResult, resultSource)
|
||||||
|
migration_manga_card_to.setOnClickListener {
|
||||||
|
controller.router.pushController(MangaController(searchResult, true).withFadeTransaction())
|
||||||
|
}
|
||||||
|
accept_migration.isEnabled = true
|
||||||
|
accept_migration.alpha = 1.0f
|
||||||
|
} else {
|
||||||
|
migration_manga_card_to.search_progress.gone()
|
||||||
|
migration_manga_card_to.search_status.text = "Found no manga"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun View.attachManga(tag: ViewTag, manga: Manga, source: Source) {
|
||||||
|
// TODO Duplicated in MangaInfoController
|
||||||
|
|
||||||
|
GlideApp.with(context)
|
||||||
|
.load(manga)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||||
|
.centerCrop()
|
||||||
|
.into(manga_cover)
|
||||||
|
|
||||||
|
manga_full_title.text = if (manga.title.isBlank()) {
|
||||||
|
context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.title
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_artist.text = if (manga.artist.isNullOrBlank()) {
|
||||||
|
context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.artist
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_author.text = if (manga.author.isNullOrBlank()) {
|
||||||
|
context.getString(R.string.unknown)
|
||||||
|
} else {
|
||||||
|
manga.author
|
||||||
|
}
|
||||||
|
|
||||||
|
manga_source.text = /*if (source.id == MERGED_SOURCE_ID) {
|
||||||
|
MergedSource.MangaConfig.readFromUrl(gson, manga.url).children.map {
|
||||||
|
sourceManager.getOrStub(it.source).toString()
|
||||||
|
}.distinct().joinToString()
|
||||||
|
} else {*/
|
||||||
|
source.toString()
|
||||||
|
// }
|
||||||
|
|
||||||
|
/*if (source.id == MERGED_SOURCE_ID) {
|
||||||
|
manga_source_label.text = "Sources"
|
||||||
|
} else {*/
|
||||||
|
manga_source_label.setText(R.string.manga_info_source_label)
|
||||||
|
// }
|
||||||
|
|
||||||
|
manga_status.setText(when (manga.status) {
|
||||||
|
SManga.ONGOING -> R.string.ongoing
|
||||||
|
SManga.COMPLETED -> R.string.completed
|
||||||
|
SManga.LICENSED -> R.string.licensed
|
||||||
|
else -> R.string.unknown
|
||||||
|
})
|
||||||
|
|
||||||
|
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||||
|
manga_chapters.text = mangaChapters.size.toString()
|
||||||
|
val latestChapter = mangaChapters.maxBy { it.chapter_number }?.chapter_number ?: -1f
|
||||||
|
val lastUpdate = Date(mangaChapters.maxBy { it.date_upload }?.date_upload ?: 0)
|
||||||
|
|
||||||
|
if (latestChapter > 0f) {
|
||||||
|
manga_last_chapter.text = DecimalFormat("#.#").format(latestChapter)
|
||||||
|
} else {
|
||||||
|
manga_last_chapter.setText(R.string.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastUpdate.time != 0L) {
|
||||||
|
manga_last_update.text = DateFormat.getDateInstance(DateFormat.SHORT).format(lastUpdate)
|
||||||
|
} else {
|
||||||
|
manga_last_update.setText(R.string.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
|
||||||
|
val objectAsView = `object` as View
|
||||||
|
container.removeView(objectAsView)
|
||||||
|
(objectAsView.tag as? ViewTag)?.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewTag(parent: CoroutineContext): CoroutineScope {
|
||||||
|
/**
|
||||||
|
* The context of this scope.
|
||||||
|
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
|
||||||
|
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
|
||||||
|
*
|
||||||
|
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
|
||||||
|
*/
|
||||||
|
override val coroutineContext = parent + Job() + Dispatchers.Default
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package exh.ui.migration.manga.process
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class MigrationProcedureConfig(
|
||||||
|
val mangaIds: List<Long>,
|
||||||
|
val targetSourceIds: List<Long>,
|
||||||
|
val useSourceWithMostChapters: Boolean,
|
||||||
|
val enableLenientSearch: Boolean,
|
||||||
|
val migrationFlags: Int,
|
||||||
|
val copy: Boolean,
|
||||||
|
val extraSearchParams: String?
|
||||||
|
): Parcelable
|
|
@ -0,0 +1,253 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.migration.manga.process
|
||||||
|
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.smartsearch.SmartSearchEngine
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
|
import eu.kanade.tachiyomi.util.await
|
||||||
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import exh.ui.migration.manga.process.MigratingManga
|
||||||
|
import exh.ui.migration.manga.process.MigrationProcedureConfig
|
||||||
|
import kotlinx.android.synthetic.main.migration_process.*
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
|
import kotlinx.coroutines.sync.withPermit
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
// TODO Will probably implode if activity is fully destroyed
|
||||||
|
class MigrationProcedureController(bundle: Bundle? = null) : BaseController(bundle), CoroutineScope {
|
||||||
|
|
||||||
|
private var titleText = "Migrate manga"
|
||||||
|
|
||||||
|
private var adapter: MigrationProcedureAdapter? = null
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext = Job() + Dispatchers.Default
|
||||||
|
|
||||||
|
val config: MigrationProcedureConfig? = args.getParcelable(CONFIG_EXTRA)
|
||||||
|
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
private val smartSearchEngine = SmartSearchEngine(coroutineContext, config?.extraSearchParams)
|
||||||
|
|
||||||
|
private var migrationsJob: Job? = null
|
||||||
|
private var migratingManga: List<MigratingManga>? = null
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
|
return inflater.inflate(R.layout.migration_process, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTitle(): String {
|
||||||
|
return titleText
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
setTitle()
|
||||||
|
val config = this.config ?: return
|
||||||
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
|
||||||
|
|
||||||
|
val newMigratingManga = migratingManga ?: run {
|
||||||
|
val new = config.mangaIds.map {
|
||||||
|
MigratingManga(db, sourceManager, it, coroutineContext)
|
||||||
|
}
|
||||||
|
migratingManga = new
|
||||||
|
new
|
||||||
|
}
|
||||||
|
|
||||||
|
adapter = MigrationProcedureAdapter(this, newMigratingManga, coroutineContext)
|
||||||
|
|
||||||
|
pager.adapter = adapter
|
||||||
|
pager.isEnabled = false
|
||||||
|
|
||||||
|
if(migrationsJob == null) {
|
||||||
|
migrationsJob = launch {
|
||||||
|
runMigrations(newMigratingManga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pager.post {
|
||||||
|
// pager.currentItem doesn't appear to be valid if we don't do this in a post
|
||||||
|
updateTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTitle() {
|
||||||
|
titleText = "Migrate manga (${pager.currentItem + 1}/${adapter?.count ?: 0})"
|
||||||
|
setTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun nextMigration() {
|
||||||
|
adapter?.let { adapter ->
|
||||||
|
if(pager.currentItem >= adapter.count - 1) {
|
||||||
|
applicationContext?.toast("All migrations complete!")
|
||||||
|
router.popCurrentController()
|
||||||
|
} else {
|
||||||
|
adapter.migratingManga[pager.currentItem].migrationJob.cancel()
|
||||||
|
pager.setCurrentItem(pager.currentItem + 1, true)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
updateTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun migrationFailure() {
|
||||||
|
activity?.let {
|
||||||
|
MaterialDialog.Builder(it)
|
||||||
|
.title("Migration failure")
|
||||||
|
.content("An unknown error occured while migrating this manga!")
|
||||||
|
.positiveText("Ok")
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun runMigrations(mangas: List<MigratingManga>) {
|
||||||
|
val sources = config?.targetSourceIds?.mapNotNull { sourceManager.get(it) as?
|
||||||
|
CatalogueSource } ?: return
|
||||||
|
|
||||||
|
for(manga in mangas) {
|
||||||
|
if(!manga.searchResult.initialized && manga.migrationJob.isActive) {
|
||||||
|
val mangaObj = manga.manga()
|
||||||
|
|
||||||
|
if(mangaObj == null) {
|
||||||
|
manga.searchResult.initialize(null)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val mangaSource = manga.mangaSource()
|
||||||
|
|
||||||
|
val result = try {
|
||||||
|
CoroutineScope(manga.migrationJob).async {
|
||||||
|
val validSources = sources.filter {
|
||||||
|
it.id != mangaSource.id
|
||||||
|
}
|
||||||
|
if(config.useSourceWithMostChapters) {
|
||||||
|
val sourceSemaphore = Semaphore(3)
|
||||||
|
val processedSources = AtomicInteger()
|
||||||
|
|
||||||
|
validSources.map { source ->
|
||||||
|
async {
|
||||||
|
sourceSemaphore.withPermit {
|
||||||
|
try {
|
||||||
|
val searchResult = if (config?.enableLenientSearch ==
|
||||||
|
true) {
|
||||||
|
smartSearchEngine.smartSearch(source, mangaObj.title)
|
||||||
|
} else {
|
||||||
|
smartSearchEngine.normalSearch(source, mangaObj.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(searchResult != null) {
|
||||||
|
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
|
||||||
|
val chapters = source.fetchChapterList(localManga).toSingle().await(Schedulers.io())
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
syncChaptersWithSource(db, chapters, localManga, source)
|
||||||
|
}
|
||||||
|
manga.progress.send(validSources.size to processedSources.incrementAndGet())
|
||||||
|
localManga to chapters.size
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch(e: CancellationException) {
|
||||||
|
// Ignore cancellations
|
||||||
|
throw e
|
||||||
|
} catch(e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.mapNotNull { it.await() }.maxBy { it.second }?.first
|
||||||
|
} else {
|
||||||
|
validSources.forEachIndexed { index, source ->
|
||||||
|
val searchResult = try {
|
||||||
|
val searchResult = if (config.enableLenientSearch) {
|
||||||
|
smartSearchEngine.smartSearch(source, mangaObj.title)
|
||||||
|
} else {
|
||||||
|
smartSearchEngine.normalSearch(source, mangaObj.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchResult != null) {
|
||||||
|
val localManga = smartSearchEngine.networkToLocalManga(searchResult, source.id)
|
||||||
|
val chapters = source.fetchChapterList(localManga).toSingle().await(Schedulers.io())
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
syncChaptersWithSource(db, chapters, localManga, source)
|
||||||
|
}
|
||||||
|
localManga
|
||||||
|
} else null
|
||||||
|
} catch(e: CancellationException) {
|
||||||
|
// Ignore cancellations
|
||||||
|
throw e
|
||||||
|
} catch(e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.progress.send(validSources.size to (index + 1))
|
||||||
|
|
||||||
|
if(searchResult != null) return@async searchResult
|
||||||
|
}
|
||||||
|
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
|
} catch(e: CancellationException) {
|
||||||
|
// Ignore canceled migrations
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result != null && result.thumbnail_url == null) {
|
||||||
|
try {
|
||||||
|
val newManga = sourceManager.getOrStub(result.source)
|
||||||
|
.fetchMangaDetails(result)
|
||||||
|
.toSingle()
|
||||||
|
.await()
|
||||||
|
result.copyFrom(newManga)
|
||||||
|
|
||||||
|
db.insertManga(result).executeAsBlocking()
|
||||||
|
} catch(e: CancellationException) {
|
||||||
|
// Ignore cancellations
|
||||||
|
throw e
|
||||||
|
} catch(e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manga.searchResult.initialize(result?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val CONFIG_EXTRA = "config_extra"
|
||||||
|
|
||||||
|
fun create(config: MigrationProcedureConfig): MigrationProcedureController {
|
||||||
|
return MigrationProcedureController(Bundle().apply {
|
||||||
|
putParcelable(CONFIG_EXTRA, config)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.smartsearch
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||||
|
import eu.kanade.tachiyomi.util.toast
|
||||||
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
class SmartSearchController(bundle: Bundle? = null) : NucleusController<SmartSearchPresenter>(), CoroutineScope {
|
||||||
|
override val coroutineContext = Job() + Dispatchers.Main
|
||||||
|
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
|
|
||||||
|
private val source = sourceManager.get(bundle?.getLong(ARG_SOURCE_ID, -1) ?: -1) as? CatalogueSource
|
||||||
|
private val smartSearchConfig: CatalogueController.SmartSearchConfig? = bundle?.getParcelable(
|
||||||
|
ARG_SMART_SEARCH_CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup) =
|
||||||
|
inflater.inflate(R.layout.smart_search, container, false)!!
|
||||||
|
|
||||||
|
override fun getTitle() = source?.name ?: ""
|
||||||
|
|
||||||
|
override fun createPresenter() = SmartSearchPresenter(source, smartSearchConfig)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View) {
|
||||||
|
super.onViewCreated(view)
|
||||||
|
|
||||||
|
appbar.bringToFront()
|
||||||
|
|
||||||
|
if(source == null || smartSearchConfig == null) {
|
||||||
|
router.popCurrentController()
|
||||||
|
applicationContext?.toast("Missing data!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init presenter now to resolve threading issues
|
||||||
|
presenter
|
||||||
|
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
for(event in presenter.smartSearchChannel) {
|
||||||
|
withContext(NonCancellable) {
|
||||||
|
if (event is SmartSearchPresenter.SearchResults.Found) {
|
||||||
|
val transaction = MangaController(event.manga, true, smartSearchConfig).withFadeTransaction()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
router.replaceTopController(transaction)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (event is SmartSearchPresenter.SearchResults.NotFound) {
|
||||||
|
applicationContext?.toast("Couldn't find the manga in the source!")
|
||||||
|
} else {
|
||||||
|
applicationContext?.toast("Error performing automatic search!")
|
||||||
|
}
|
||||||
|
|
||||||
|
val transaction = BrowseCatalogueController(source, smartSearchConfig.origTitle, smartSearchConfig).withFadeTransaction()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
router.replaceTopController(transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val ARG_SOURCE_ID = "SOURCE_ID"
|
||||||
|
const val ARG_SMART_SEARCH_CONFIG = "SMART_SEARCH_CONFIG"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.smartsearch
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.smartsearch.SmartSearchEngine
|
||||||
|
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SmartSearchPresenter(private val source: CatalogueSource?, private val config: CatalogueController.SmartSearchConfig?):
|
||||||
|
BasePresenter<SmartSearchController>(), CoroutineScope {
|
||||||
|
|
||||||
|
override val coroutineContext = Job() + Dispatchers.Main
|
||||||
|
|
||||||
|
val smartSearchChannel = Channel<SearchResults>()
|
||||||
|
|
||||||
|
private val smartSearchEngine = SmartSearchEngine(coroutineContext)
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
if(source != null && config != null) {
|
||||||
|
launch(Dispatchers.Default) {
|
||||||
|
val result = try {
|
||||||
|
val resultManga = smartSearchEngine.smartSearch(source, config.origTitle)
|
||||||
|
if (resultManga != null) {
|
||||||
|
val localManga = smartSearchEngine.networkToLocalManga(resultManga, source.id)
|
||||||
|
SearchResults.Found(localManga)
|
||||||
|
} else {
|
||||||
|
SearchResults.NotFound
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) {
|
||||||
|
throw e
|
||||||
|
} else {
|
||||||
|
SearchResults.Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
smartSearchChannel.send(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
super.onDestroy()
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SearchEntry(val manga: SManga, val dist: Double)
|
||||||
|
|
||||||
|
sealed class SearchResults {
|
||||||
|
data class Found(val manga: Manga): SearchResults()
|
||||||
|
object NotFound: SearchResults()
|
||||||
|
object Error: SearchResults()
|
||||||
|
}
|
||||||
|
}
|
48
app/src/main/java/eu/kanade/tachiyomi/util/DeferredField.kt
Normal file
48
app/src/main/java/eu/kanade/tachiyomi/util/DeferredField.kt
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
|
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Field that can be initialized later. Users can suspend while waiting for the field to initialize.
|
||||||
|
*
|
||||||
|
* @author nulldev
|
||||||
|
*/
|
||||||
|
class DeferredField<T> {
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
private var content: T? = null
|
||||||
|
|
||||||
|
@Volatile
|
||||||
|
var initialized = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
private val mutex = Mutex(true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the field
|
||||||
|
*/
|
||||||
|
fun initialize(content: T) {
|
||||||
|
// Fast-path new listeners
|
||||||
|
this.content = content
|
||||||
|
initialized = true
|
||||||
|
|
||||||
|
// Notify current listeners
|
||||||
|
mutex.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will only suspend if !initialized.
|
||||||
|
*/
|
||||||
|
suspend fun get(): T {
|
||||||
|
// Check if field is initialized and return immediately if it is
|
||||||
|
if (initialized) return content as T
|
||||||
|
|
||||||
|
// Wait for field to initialize
|
||||||
|
mutex.withLock {}
|
||||||
|
|
||||||
|
// Field is initialized, return value
|
||||||
|
return content as T
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/java/eu/kanade/tachiyomi/util/RxUtil.kt
Normal file
26
app/src/main/java/eu/kanade/tachiyomi/util/RxUtil.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import rx.Scheduler
|
||||||
|
import rx.Single
|
||||||
|
import rx.Subscription
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
suspend fun <T> Single<T>.await(subscribeOn: Scheduler? = null): T {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
val self = if (subscribeOn != null) subscribeOn(subscribeOn) else this
|
||||||
|
lateinit var sub: Subscription
|
||||||
|
sub = self.subscribe({
|
||||||
|
continuation.resume(it) {
|
||||||
|
sub.unsubscribe()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
if (!continuation.isCancelled)
|
||||||
|
continuation.resumeWithException(it)
|
||||||
|
})
|
||||||
|
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
sub.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
207
app/src/main/res/layout/migration_design_controller.xml
Normal file
207
app/src/main/res/layout/migration_design_controller.xml
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/textView2"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:listitem="@layout/migration_source_item">
|
||||||
|
|
||||||
|
</androidx.recyclerview.widget.RecyclerView>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/data_to_include_in_migration"
|
||||||
|
android:textAppearance="@style/TextAppearance.Medium.Body2"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/mig_chapters"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/textView" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/mig_chapters"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/chapters"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/textView"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/textView2" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/mig_categories"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/categories"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/mig_chapters"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/mig_chapters" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/mig_tracking"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/track"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/mig_categories"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/mig_categories" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/textView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/options"
|
||||||
|
android:textAppearance="@style/TextAppearance.Medium.Body2"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/prioritize_chapter_count"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/prioritize_chapter_count"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/textView"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/migration_mode" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/migration_mode"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:clickable="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/fuzzy_search"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/use_smart_search"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/textView"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/fuzzy_search" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/fuzzy_search"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:text="@string/use_intelligent_search"
|
||||||
|
android:clickable="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/copy_manga"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/copy_manga"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/textView"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/copy_manga_desc" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/copy_manga_desc"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:text="@string/keep_old_manga"
|
||||||
|
android:clickable="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/extra_search_param"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.SwitchCompat
|
||||||
|
android:id="@+id/extra_search_param"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintStart_toStartOf="@+id/textView"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/extra_search_param_desc" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/extra_search_param_desc"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:text="@string/include_extra_search_parameter"
|
||||||
|
android:clickable="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/extra_search_param_text"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/prioritize_chapter_count"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/extra_search_param_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:ems="10"
|
||||||
|
android:hint="@string/search_parameter"
|
||||||
|
android:inputType="textPersonName"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/begin_migration_btn"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:importantForAutofill="no" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/begin_migration_btn"
|
||||||
|
style="@style/Theme.Widget.Button.Colored"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="@string/begin_migration"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/options_group"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:constraint_referenced_ids="migration_mode,use_smart_search,fuzzy_search,copy_manga,extra_search_param_desc,mig_tracking,textView,mig_chapters,copy_manga_desc,textView2,prioritize_chapter_count,mig_categories,extra_search_param" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
270
app/src/main/res/layout/migration_manga_card.xml
Normal file
270
app/src/main/res/layout/migration_manga_card.xml
Normal file
|
@ -0,0 +1,270 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/manga_cover"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:contentDescription="@string/description_cover"
|
||||||
|
app:layout_constraintDimensionRatio="l,2:3"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toLeftOf="@+id/card_scroll_content"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_min="100dp"
|
||||||
|
tools:background="@color/material_grey_700" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/card_scroll_content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
app:layout_constraintHorizontal_weight="2"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_cover"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/manga_cover">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_full_title"
|
||||||
|
style="@style/TextAppearance.Medium.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:text="@string/manga_info_full_title_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:autoSizeMaxTextSize="20sp"
|
||||||
|
app:autoSizeMinTextSize="12sp"
|
||||||
|
app:autoSizeStepGranularity="2sp"
|
||||||
|
app:autoSizeTextType="uniform"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_author_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:text="@string/manga_info_author_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_full_title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_author"
|
||||||
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_author_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_author_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_artist_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:text="@string/manga_info_artist_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_author_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_artist"
|
||||||
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_artist_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_artist_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_status_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:text="@string/manga_info_status_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_artist_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_status"
|
||||||
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintBaseline_toBaselineOf="@+id/manga_status_label"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_status_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_chapters_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:text="@string/manga_info_chapters_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_chapters"
|
||||||
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_chapters_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_status_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_last_chapter_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:text="@string/manga_info_last_chapter_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_chapters_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_last_chapter"
|
||||||
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_last_chapter_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_chapters_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_last_update_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:text="@string/manga_info_latest_data_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_last_chapter_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_last_update"
|
||||||
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_last_update_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_last_chapter_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_source_label"
|
||||||
|
style="@style/TextAppearance.Medium.Body2"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:text="@string/manga_info_source_label"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_last_update_label" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/manga_source"
|
||||||
|
style="@style/TextAppearance.Regular.Body1.Secondary"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:textIsSelectable="false"
|
||||||
|
app:layout_constraintLeft_toRightOf="@+id/manga_source_label"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/manga_last_update_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/card_shim"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:alpha="0.9"
|
||||||
|
android:background="?attr/background_card"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/search_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="Searching..."
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Large"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/search_progress"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/card_shim"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/card_shim" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/search_progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/card_shim"
|
||||||
|
app:layout_constraintEnd_toEndOf="@+id/card_shim"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Group
|
||||||
|
android:id="@+id/loading_group"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:constraint_referenced_ids="card_shim,search_status,search_progress" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.cardview.widget.CardView>
|
18
app/src/main/res/layout/migration_process.xml
Normal file
18
app/src/main/res/layout/migration_process.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?attr/colorPrimary" >
|
||||||
|
|
||||||
|
<eu.kanade.tachiyomi.ui.migration.manga.process.DeactivatableViewPager
|
||||||
|
android:id="@+id/pager"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
108
app/src/main/res/layout/migration_process_item.xml
Normal file
108
app/src/main/res/layout/migration_process_item.xml
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/migration_manga_card_from"
|
||||||
|
layout="@layout/migration_manga_card"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_max="450dp" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/imageView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:contentDescription="migrating to"
|
||||||
|
android:scaleType="center"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/migration_manga_card_from"
|
||||||
|
app:srcCompat="@drawable/ic_arrow_down_white_32dp" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/migration_manga_card_to"
|
||||||
|
layout="@layout/migration_manga_card"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:layout_marginLeft="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/imageView"
|
||||||
|
app:layout_constraintWidth_max="450dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/skip_migration"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:drawableStart="@drawable/ic_clear_grey_24dp_img"
|
||||||
|
android:drawablePadding="6dp"
|
||||||
|
android:text="Skip manga"
|
||||||
|
android:textColor="#ffffff"
|
||||||
|
app:backgroundTint="#E53935"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/accept_migration"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/accept_migration"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:alpha="0.5"
|
||||||
|
android:drawableStart="@drawable/ic_check_box_24dp"
|
||||||
|
android:drawablePadding="6dp"
|
||||||
|
android:enabled="false"
|
||||||
|
android:text="Migrate manga"
|
||||||
|
android:textColor="#ffffff"
|
||||||
|
app:backgroundTint="#00C853"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/skip_migration"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.5"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/skip_migration" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/migrating_frame"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/migrating_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#E6FFFFFF" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/migrating_progress"
|
||||||
|
style="?android:attr/progressBarStyle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
43
app/src/main/res/layout/migration_source_item.xml
Normal file
43
app/src/main/res/layout/migration_source_item.xml
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||||
|
android:background="?attr/selectable_list_drawable">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/image"
|
||||||
|
android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||||
|
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||||
|
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
||||||
|
android:paddingStart="@dimen/material_component_lists_icon_left_padding"
|
||||||
|
android:paddingRight="0dp"
|
||||||
|
android:paddingEnd="0dp"
|
||||||
|
tools:src="@mipmap/ic_launcher_round"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="@dimen/material_component_lists_text_left_padding"
|
||||||
|
android:layout_marginStart="@dimen/material_component_lists_text_left_padding"
|
||||||
|
android:layout_marginRight="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||||
|
android:layout_marginEnd="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
||||||
|
tools:text="Title"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/reorder"
|
||||||
|
android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||||
|
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
app:srcCompat="@drawable/ic_reorder_grey_24dp"
|
||||||
|
android:tint="?android:attr/textColorPrimary"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
57
app/src/main/res/layout/smart_search.xml
Normal file
57
app/src/main/res/layout/smart_search.xml
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:elevation="0dp">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?attr/actionBarSize"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:theme="?attr/actionBarTheme" />
|
||||||
|
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/intercept_status"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="Searching source..."
|
||||||
|
android:textAppearance="@style/TextAppearance.Medium.Title"
|
||||||
|
android:textColor="@android:color/white" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/intercept_progress"
|
||||||
|
style="?android:attr/progressBarStyleLarge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminateTint="@android:color/white" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -551,5 +551,11 @@
|
||||||
<string name="channel_ext_updates">Extension Updates</string>
|
<string name="channel_ext_updates">Extension Updates</string>
|
||||||
<string name="channel_library_updates">Updating Library</string>
|
<string name="channel_library_updates">Updating Library</string>
|
||||||
<string name="channel_new_chapters">New Chapters</string>
|
<string name="channel_new_chapters">New Chapters</string>
|
||||||
|
<string name="data_to_include_in_migration">Data to include in migration</string>
|
||||||
|
<string name="search_parameter">Search parameter (e.g. language:english)</string>
|
||||||
|
<string name="include_extra_search_parameter">Include extra search parameter when searching</string>
|
||||||
|
<string name="keep_old_manga">Keep old manga</string>
|
||||||
|
<string name="use_intelligent_search">Use intelligent search algorithm</string>
|
||||||
|
<string name="begin_migration">Begin migration</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Reference in a new issue