Added Select all and source migration to selected manga on library screen
@ -126,6 +126,16 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
||||
|
||||
subscriptions += controller.selectionRelay
|
||||
.subscribe { onSelectionChanged(it) }
|
||||
|
||||
subscriptions += controller.selectAllRelay
|
||||
.subscribe {
|
||||
if (it == category.id) {
|
||||
adapter.currentItems.forEach { item ->
|
||||
controller.setSelection(item.manga, true)
|
||||
}
|
||||
controller.invalidateActionMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onRecycle() {
|
||||
|
@ -5,20 +5,24 @@ import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.ActionBarContextView
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.GravityCompat
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.f2prateek.rx.preferences.Preference
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.jakewharton.rxbinding.support.v4.view.pageSelections
|
||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChanges
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
@ -37,7 +41,15 @@ import eu.kanade.tachiyomi.ui.category.CategoryController
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import eu.kanade.tachiyomi.ui.migration.MigrationInterface
|
||||
import eu.kanade.tachiyomi.ui.migration.SearchController
|
||||
import eu.kanade.tachiyomi.util.doOnApplyWindowInsets
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.util.marginBottom
|
||||
import eu.kanade.tachiyomi.util.marginTop
|
||||
import eu.kanade.tachiyomi.util.snack
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.util.updatePaddingRelative
|
||||
import kotlinx.android.synthetic.main.library_controller.*
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import rx.Subscription
|
||||
@ -46,7 +58,6 @@ import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class LibraryController(
|
||||
bundle: Bundle? = null,
|
||||
private val preferences: PreferencesHelper = Injekt.get()
|
||||
@ -55,7 +66,8 @@ class LibraryController(
|
||||
SecondaryDrawerController,
|
||||
ActionMode.Callback,
|
||||
ChangeMangaCategoriesDialog.Listener,
|
||||
DeleteLibraryMangasDialog.Listener {
|
||||
DeleteLibraryMangasDialog.Listener,
|
||||
MigrationInterface {
|
||||
|
||||
/**
|
||||
* Position of the active category.
|
||||
@ -78,6 +90,11 @@ class LibraryController(
|
||||
*/
|
||||
val selectedMangas = mutableSetOf<Manga>()
|
||||
|
||||
/**
|
||||
* Current mangas to move.
|
||||
*/
|
||||
private var migratingMangas = mutableSetOf<Manga>()
|
||||
|
||||
private var selectedCoverManga: Manga? = null
|
||||
|
||||
/**
|
||||
@ -95,6 +112,11 @@ class LibraryController(
|
||||
*/
|
||||
val libraryMangaRelay: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
|
||||
|
||||
/**
|
||||
* Relay to notify the library's viewpager to select all manga
|
||||
*/
|
||||
val selectAllRelay: PublishRelay<Int> = PublishRelay.create()
|
||||
|
||||
/**
|
||||
* Number of manga per row in grid mode.
|
||||
*/
|
||||
@ -425,11 +447,36 @@ class LibraryController(
|
||||
}
|
||||
R.id.action_move_to_category -> showChangeMangaCategoriesDialog()
|
||||
R.id.action_delete -> deleteMangasFromLibrary()
|
||||
R.id.action_select_all -> {
|
||||
adapter?.categories?.getOrNull(library_pager.currentItem)?.id?.let {
|
||||
selectAllRelay.call(it)
|
||||
}
|
||||
}
|
||||
R.id.action_migrate -> startMangaMigration()
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean): Manga? {
|
||||
presenter.migrateManga(prevManga, manga, replace = replace)
|
||||
val nextManga = migratingMangas.firstOrNull() ?: return null
|
||||
migratingMangas.remove(nextManga)
|
||||
return nextManga
|
||||
}
|
||||
|
||||
private fun startMangaMigration() {
|
||||
migratingMangas.clear()
|
||||
migratingMangas.addAll(selectedMangas)
|
||||
destroyActionModeIfNeeded()
|
||||
val manga = migratingMangas.firstOrNull() ?: return
|
||||
val searchController = SearchController(manga)
|
||||
searchController.totalProgress = migratingMangas.size
|
||||
searchController.targetController = this
|
||||
router.pushController(searchController.withFadeTransaction())
|
||||
migratingMangas.remove(manga)
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||
// Clear all the manga selections and notify child views.
|
||||
selectedMangas.clear()
|
||||
|
@ -11,12 +11,16 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
|
||||
import eu.kanade.tachiyomi.util.combineLatest
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
@ -355,6 +359,79 @@ class LibraryPresenter(
|
||||
db.setMangaCategories(mc, mangas)
|
||||
}
|
||||
|
||||
fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean) {
|
||||
val source = sourceManager.get(manga.source) ?: return
|
||||
|
||||
//state = state.copy(isReplacingManga = true)
|
||||
|
||||
Observable.defer { source.fetchChapterList(manga) }
|
||||
.onErrorReturn { emptyList() }
|
||||
.doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) }
|
||||
.onErrorReturn { emptyList() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
//.doOnUnsubscribe { state = state.copy(isReplacingManga = false) }
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun migrateMangaInternal(source: Source, sourceChapters: List<SChapter>,
|
||||
prevManga: Manga, manga: Manga, replace: Boolean) {
|
||||
|
||||
val flags = preferences.migrateFlags().getOrDefault()
|
||||
val migrateChapters = MigrationFlags.hasChapters(flags)
|
||||
val migrateCategories = MigrationFlags.hasCategories(flags)
|
||||
val migrateTracks = MigrationFlags.hasTracks(flags)
|
||||
|
||||
db.inTransaction {
|
||||
// Update chapters read
|
||||
if (migrateChapters) {
|
||||
try {
|
||||
syncChaptersWithSource(db, sourceChapters, manga, source)
|
||||
} catch (e: Exception) {
|
||||
// Worst case, chapters won't be synced
|
||||
}
|
||||
|
||||
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 (migrateCategories) {
|
||||
val categories = db.getCategoriesForManga(prevManga).executeAsBlocking()
|
||||
val mangaCategories = categories.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mangaCategories, listOf(manga))
|
||||
}
|
||||
// Update track
|
||||
if (migrateTracks) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cover with local file.
|
||||
*
|
||||
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.migration
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -20,7 +19,8 @@ import kotlinx.android.synthetic.main.migration_controller.*
|
||||
|
||||
class MigrationController : NucleusController<MigrationPresenter>(),
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
SourceAdapter.OnSelectClickListener {
|
||||
SourceAdapter.OnSelectClickListener,
|
||||
MigrationInterface {
|
||||
|
||||
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||
|
||||
@ -38,6 +38,13 @@ class MigrationController : NucleusController<MigrationPresenter>(),
|
||||
return inflater.inflate(R.layout.migration_controller, container, false)
|
||||
}
|
||||
|
||||
fun searchController(manga:Manga): SearchController {
|
||||
val controller = SearchController(manga)
|
||||
controller.targetController = this
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
@ -112,12 +119,9 @@ class MigrationController : NucleusController<MigrationPresenter>(),
|
||||
onItemClick(view, position)
|
||||
}
|
||||
|
||||
fun migrateManga(prevManga: Manga, manga: Manga) {
|
||||
presenter.migrateManga(prevManga, manga, replace = true)
|
||||
}
|
||||
|
||||
fun copyManga(prevManga: Manga, manga: Manga) {
|
||||
presenter.migrateManga(prevManga, manga, replace = false)
|
||||
override fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean): Manga? {
|
||||
presenter.migrateManga(prevManga, manga, replace)
|
||||
return null
|
||||
}
|
||||
|
||||
class LoadingController : DialogController() {
|
||||
@ -135,4 +139,8 @@ class MigrationController : NucleusController<MigrationPresenter>(),
|
||||
const val LOADING_DIALOG_TAG = "LoadingDialog"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
interface MigrationInterface {
|
||||
fun migrateManga(prevManga: Manga, manga: Manga, replace: Boolean): Manga?
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@ -17,6 +18,16 @@ class SearchController(
|
||||
) : CatalogueSearchController(manga?.title) {
|
||||
|
||||
private var newManga: Manga? = null
|
||||
private var progress = 1
|
||||
var totalProgress = 0
|
||||
|
||||
override fun getTitle(): String? {
|
||||
if (totalProgress > 1) {
|
||||
return "($progress/$totalProgress) ${super.getTitle()}"
|
||||
}
|
||||
else
|
||||
return super.getTitle()
|
||||
}
|
||||
|
||||
override fun createPresenter(): CatalogueSearchPresenter {
|
||||
return SearchPresenter(initialQuery, manga!!)
|
||||
@ -35,21 +46,32 @@ class SearchController(
|
||||
}
|
||||
|
||||
fun migrateManga() {
|
||||
val target = targetController as? MigrationController ?: return
|
||||
val target = targetController as? MigrationInterface ?: return
|
||||
val manga = manga ?: return
|
||||
val newManga = newManga ?: return
|
||||
|
||||
router.popController(this)
|
||||
target.migrateManga(manga, newManga)
|
||||
val nextManga = target.migrateManga(manga, newManga, true)
|
||||
replaceWithNewSearchController(nextManga)
|
||||
}
|
||||
|
||||
fun copyManga() {
|
||||
val target = targetController as? MigrationController ?: return
|
||||
val target = targetController as? MigrationInterface ?: return
|
||||
val manga = manga ?: return
|
||||
val newManga = newManga ?: return
|
||||
|
||||
router.popController(this)
|
||||
target.copyManga(manga, newManga)
|
||||
val nextManga = target.migrateManga(manga, newManga, false)
|
||||
replaceWithNewSearchController(nextManga)
|
||||
}
|
||||
|
||||
private fun replaceWithNewSearchController(manga: Manga?) {
|
||||
if (manga != null) {
|
||||
router.popCurrentController()
|
||||
val searchController = SearchController(manga)
|
||||
searchController.targetController = targetController
|
||||
searchController.progress = progress + 1
|
||||
searchController.totalProgress = totalProgress
|
||||
router.replaceTopController(searchController.withFadeTransaction())
|
||||
} else router.popController(this)
|
||||
}
|
||||
|
||||
override fun onMangaClick(manga: Manga) {
|
||||
|
BIN
app/src/main/res/drawable-hdpi/baseline_swap_calls_white_18.png
Normal file
After Width: | Height: | Size: 248 B |
BIN
app/src/main/res/drawable-hdpi/baseline_swap_calls_white_24.png
Normal file
After Width: | Height: | Size: 291 B |
BIN
app/src/main/res/drawable-hdpi/baseline_swap_calls_white_36.png
Normal file
After Width: | Height: | Size: 426 B |
BIN
app/src/main/res/drawable-hdpi/baseline_swap_calls_white_48.png
Normal file
After Width: | Height: | Size: 443 B |
BIN
app/src/main/res/drawable-mdpi/baseline_swap_calls_white_18.png
Normal file
After Width: | Height: | Size: 204 B |
BIN
app/src/main/res/drawable-mdpi/baseline_swap_calls_white_24.png
Normal file
After Width: | Height: | Size: 186 B |
BIN
app/src/main/res/drawable-mdpi/baseline_swap_calls_white_36.png
Normal file
After Width: | Height: | Size: 291 B |
BIN
app/src/main/res/drawable-mdpi/baseline_swap_calls_white_48.png
Normal file
After Width: | Height: | Size: 323 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_swap_calls_white_18.png
Normal file
After Width: | Height: | Size: 291 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_swap_calls_white_24.png
Normal file
After Width: | Height: | Size: 323 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_swap_calls_white_36.png
Normal file
After Width: | Height: | Size: 443 B |
BIN
app/src/main/res/drawable-xhdpi/baseline_swap_calls_white_48.png
Normal file
After Width: | Height: | Size: 580 B |
After Width: | Height: | Size: 426 B |
After Width: | Height: | Size: 443 B |
After Width: | Height: | Size: 740 B |
After Width: | Height: | Size: 872 B |
After Width: | Height: | Size: 443 B |
After Width: | Height: | Size: 580 B |
After Width: | Height: | Size: 872 B |
After Width: | Height: | Size: 1.2 KiB |
10
app/src/main/res/drawable/baseline_swap_calls_24.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,4l-4,4h3v7c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2V8c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8v7H2l4,4 4,-4H7V8c0,-1.1 0.9,-2 2,-2s2,0.9 2,2v7c0,2.21 1.79,4 4,4s4,-1.79 4,-4V8h3l-4,-4z"/>
|
||||
</vector>
|
@ -18,4 +18,15 @@
|
||||
android:icon="@drawable/ic_delete_white_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item android:id="@+id/action_select_all"
|
||||
android:title="@string/action_select_all"
|
||||
android:icon="@drawable/ic_select_all_white_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_migrate"
|
||||
android:icon="@drawable/baseline_swap_calls_white_24"
|
||||
android:title="@string/label_migration"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|