mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-09-10 20:43:14 +02:00
Implemented Auto Source Migration
Co-Authored-By: NerdNumber9 <nerdnumber9@users.noreply.github.com>
This commit is contained in:
parent
c7dabb9d63
commit
8ba75831e6
@ -243,6 +243,9 @@ dependencies {
|
||||
final coroutines_version = '1.3.2'
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
|
||||
|
||||
// Text distance
|
||||
implementation 'info.debatty:java-string-similarity:1.2.1'
|
||||
}
|
||||
|
||||
buildscript {
|
||||
|
@ -1,18 +1,37 @@
|
||||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import android.content.Context
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
|
||||
import eu.kanade.tachiyomi.data.database.mappers.*
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.data.database.queries.*
|
||||
import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
|
||||
import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
|
||||
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
|
||||
|
||||
/**
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
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)
|
||||
.name(DbOpenCallback.DATABASE_NAME)
|
||||
@ -26,6 +45,7 @@ open class DatabaseHelper(context: Context)
|
||||
.addTypeMapping(Track::class.java, TrackTypeMapping())
|
||||
.addTypeMapping(Category::class.java, CategoryTypeMapping())
|
||||
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
|
||||
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
|
||||
.addTypeMapping(History::class.java, HistoryTypeMapping())
|
||||
.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
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
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.util.RecyclerWindowInsetsListener
|
||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@ -239,4 +241,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
||||
}
|
||||
|
||||
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.SecondaryDrawerController
|
||||
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.HeightTopWindowInsetsListener
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
@ -68,6 +69,18 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
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 {
|
||||
putLong(SOURCE_ID_KEY, source.id)
|
||||
})
|
||||
@ -579,6 +592,9 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
|
||||
protected companion object {
|
||||
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.MigrationInterface
|
||||
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.inflate
|
||||
import eu.kanade.tachiyomi.util.marginBottom
|
||||
@ -467,7 +468,13 @@ class LibraryController(
|
||||
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 -> {
|
||||
val showAll = (selectedMangas.filter { (it as? LibraryManga)?.hide_title == true }
|
||||
).size == selectedMangas.size
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
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.TabbedController
|
||||
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.info.MangaInfoController
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
||||
@ -40,6 +40,21 @@ import java.util.Date
|
||||
|
||||
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) :
|
||||
super
|
||||
(Bundle()
|
||||
@ -213,6 +228,10 @@ class MangaController : RxController, TabbedController {
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
const val UPDATE_EXTRA = "update"
|
||||
const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
|
||||
|
||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||
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.items.IFlexible
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||
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.await
|
||||
import eu.kanade.tachiyomi.util.launchUI
|
||||
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>(),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
SourceAdapter.OnSelectClickListener,
|
||||
SourceAdapter.OnAutoClickListener,
|
||||
MigrationInterface {
|
||||
|
||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
@ -119,6 +129,19 @@ class MigrationController : NucleusController<MigrationPresenter>(),
|
||||
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? {
|
||||
presenter.migrateManga(prevManga, manga, replace)
|
||||
return null
|
||||
|
@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.R
|
||||
|
||||
object MigrationFlags {
|
||||
|
||||
private const val CHAPTERS = 0b001
|
||||
private const val CATEGORIES = 0b010
|
||||
private const val TRACK = 0b100
|
||||
const val CHAPTERS = 0b001
|
||||
const val CATEGORIES = 0b010
|
||||
const val TRACK = 0b100
|
||||
|
||||
private const val CHAPTERS2 = 0x1
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<*>>?) {
|
||||
if (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.SlicedHolder
|
||||
import eu.kanade.tachiyomi.util.getRound
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
import io.github.mthli.slice.Slice
|
||||
import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.*
|
||||
|
||||
@ -21,11 +20,14 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
|
||||
get() = card
|
||||
|
||||
init {
|
||||
source_latest.gone()
|
||||
source_latest.text = "Auto"
|
||||
source_browse.setText(R.string.select)
|
||||
source_browse.setOnClickListener {
|
||||
adapter.selectClickListener?.onSelectClick(adapterPosition)
|
||||
}
|
||||
source_latest.setOnClickListener {
|
||||
adapter.autoClickListener?.onAutoClick(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
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_library_updates">Updating Library</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>
|
||||
|
Loading…
Reference in New Issue
Block a user