diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 17348c5b1..a5b3a4da8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver +import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.tables.CategoryTable @@ -74,6 +75,11 @@ interface MangaQueries : DbProvider { .withPutResolver(MangaLastUpdatedPutResolver()) .prepare() + fun updateMangaFavorite(manga: Manga) = db.put() + .`object`(manga) + .withPutResolver(MangaFavoritePutResolver()) + .prepare() + fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteMangas(mangas: List) = db.delete().objects(mangas).prepare() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt new file mode 100644 index 000000000..c0057d213 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaFavoritePutResolver.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.data.database.resolvers + +import android.content.ContentValues +import com.pushtorefresh.storio.sqlite.StorIOSQLite +import com.pushtorefresh.storio.sqlite.operations.put.PutResolver +import com.pushtorefresh.storio.sqlite.operations.put.PutResult +import com.pushtorefresh.storio.sqlite.queries.UpdateQuery +import eu.kanade.tachiyomi.data.database.inTransactionReturn +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.tables.MangaTable + +class MangaFavoritePutResolver : PutResolver() { + + override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn { + val updateQuery = mapToUpdateQuery(manga) + val contentValues = mapToContentValues(manga) + + val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues) + PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) + } + + fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() + .table(MangaTable.TABLE) + .where("${MangaTable.COL_ID} = ?") + .whereArgs(manga.id) + .build() + + fun mapToContentValues(manga: Manga) = ContentValues(1).apply { + put(MangaTable.COL_FAVORITE, manga.favorite) + } + +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 00bfc6661..9b1605b5d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -165,4 +165,10 @@ class PreferencesHelper(val context: Context) { fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1) + fun migrateChapters() = rxPrefs.getBoolean("migrate_chapters", true) + + fun migrateTracks() = rxPrefs.getBoolean("migrate_tracks", true) + + fun migrateCategories() = rxPrefs.getBoolean("migrate_categories", true) + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt new file mode 100644 index 000000000..b2fc8fd26 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/holder/SlicedHolder.kt @@ -0,0 +1,71 @@ +package eu.kanade.tachiyomi.ui.base.holder + +import android.os.Build +import android.view.View +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.flexibleadapter.items.ISectionable +import eu.kanade.tachiyomi.util.dpToPx +import io.github.mthli.slice.Slice + +interface SlicedHolder { + + val slice: Slice + + val adapter: FlexibleAdapter> + + val viewToSlice: View + + fun setCardEdges(item: ISectionable<*, *>) { + // Position of this item in its header. Defaults to 0 when header is null. + var position = 0 + + // Number of items in the header of this item. Defaults to 1 when header is null. + var count = 1 + + if (item.header != null) { + val sectionItems = adapter.getSectionItems(item.header) + position = sectionItems.indexOf(item) + count = sectionItems.size + } + + when { + // Only one item in the card + count == 1 -> applySlice(2f, false, false, true, true) + // First item of the card + position == 0 -> applySlice(2f, false, true, true, false) + // Last item of the card + position == count - 1 -> applySlice(2f, true, false, false, true) + // Middle item + else -> applySlice(0f, false, false, false, false) + } + } + + private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, + topShadow: Boolean, bottomShadow: Boolean) { + val margin = margin + + slice.setRadius(radius) + slice.showLeftTopRect(topRect) + slice.showRightTopRect(topRect) + slice.showLeftBottomRect(bottomRect) + slice.showRightBottomRect(bottomRect) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + slice.showTopEdgeShadow(topShadow) + slice.showBottomEdgeShadow(bottomShadow) + } + setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) + } + + private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { + if (viewToSlice.layoutParams is ViewGroup.MarginLayoutParams) { + val p = viewToSlice.layoutParams as ViewGroup.MarginLayoutParams + p.setMargins(left, top, right, bottom) + } + } + + val margin + get() = 8.dpToPx + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt index d60106cbe..b5ac65018 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceDividerItemDecoration.kt @@ -18,17 +18,17 @@ class SourceDividerItemDecoration(context: Context) : RecyclerView.ItemDecoratio } override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - val left = parent.paddingLeft + SourceHolder.margin - val right = parent.width - parent.paddingRight - SourceHolder.margin - val childCount = parent.childCount for (i in 0 until childCount - 1) { val child = parent.getChildAt(i) - if (parent.getChildViewHolder(child) is SourceHolder && + val holder = parent.getChildViewHolder(child) + if (holder is SourceHolder && parent.getChildViewHolder(parent.getChildAt(i + 1)) is SourceHolder) { val params = child.layoutParams as RecyclerView.LayoutParams val top = child.bottom + params.bottomMargin val bottom = top + divider.intrinsicHeight + val left = parent.paddingLeft + holder.margin + val right = parent.paddingRight + holder.margin divider.setBounds(left, top, right, bottom) divider.draw(c) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt index 5a052d0e9..a4a1585c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/SourceHolder.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.source.online.LoginSource import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder import eu.kanade.tachiyomi.util.dpToPx import eu.kanade.tachiyomi.util.getRound import eu.kanade.tachiyomi.util.gone @@ -13,12 +14,17 @@ import eu.kanade.tachiyomi.util.visible import io.github.mthli.slice.Slice import kotlinx.android.synthetic.main.catalogue_main_controller_card_item.* -class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHolder(view, adapter) { +class SourceHolder(view: View, override val adapter: CatalogueAdapter) : + BaseFlexibleViewHolder(view, adapter), + SlicedHolder { - private val slice = Slice(card).apply { + override val slice = Slice(card).apply { setColor(adapter.cardBackground) } + override val viewToSlice: View + get() = card + init { source_browse.setOnClickListener { adapter.browseClickListener.onBrowseClick(adapterPosition) @@ -50,56 +56,4 @@ class SourceHolder(view: View, adapter: CatalogueAdapter) : BaseFlexibleViewHold source_latest.visible() } } - - private fun setCardEdges(item: SourceItem) { - // Position of this item in its header. Defaults to 0 when header is null. - var position = 0 - - // Number of items in the header of this item. Defaults to 1 when header is null. - var count = 1 - - if (item.header != null) { - val sectionItems = mAdapter.getSectionItems(item.header) - position = sectionItems.indexOf(item) - count = sectionItems.size - } - - when { - // Only one item in the card - count == 1 -> applySlice(2f, false, false, true, true) - // First item of the card - position == 0 -> applySlice(2f, false, true, true, false) - // Last item of the card - position == count - 1 -> applySlice(2f, true, false, false, true) - // Middle item - else -> applySlice(0f, false, false, false, false) - } - } - - private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean, - topShadow: Boolean, bottomShadow: Boolean) { - - slice.setRadius(radius) - slice.showLeftTopRect(topRect) - slice.showRightTopRect(topRect) - slice.showLeftBottomRect(bottomRect) - slice.showRightBottomRect(bottomRect) - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - slice.showTopEdgeShadow(topShadow) - slice.showBottomEdgeShadow(bottomShadow) - } - setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0) - } - - private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) { - val v = card - if (v.layoutParams is ViewGroup.MarginLayoutParams) { - val p = v.layoutParams as ViewGroup.MarginLayoutParams - p.setMargins(left, top, right, bottom) - } - } - - companion object { - val margin = 8.dpToPx - } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt index dec1dcc8d..bfae37552 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchController.kt @@ -18,7 +18,7 @@ import kotlinx.android.synthetic.main.catalogue_global_search_controller.* * This controller should only handle UI actions, IO actions should be done by [CatalogueSearchPresenter] * [CatalogueSearchCardAdapter.OnMangaClickListener] called when manga is clicked in global search */ -class CatalogueSearchController(private val initialQuery: String? = null) : +open class CatalogueSearchController(protected val initialQuery: String? = null) : NucleusController(), CatalogueSearchCardAdapter.OnMangaClickListener { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt index fac3a5b6c..e12f34b2e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/global_search/CatalogueSearchPresenter.kt @@ -30,7 +30,7 @@ import uy.kohesive.injekt.api.get * @param db manages the database calls. * @param preferencesHelper manages the preference calls. */ -class CatalogueSearchPresenter( +open class CatalogueSearchPresenter( val initialQuery: String? = "", val sourceManager: SourceManager = Injekt.get(), val db: DatabaseHelper = Injekt.get(), @@ -86,7 +86,7 @@ class CatalogueSearchPresenter( * * @return list containing enabled sources. */ - private fun getEnabledSources(): List { + protected open fun getEnabledSources(): List { val languages = preferencesHelper.enabledLanguages().getOrDefault() val hiddenCatalogues = preferencesHelper.hiddenCatalogues().getOrDefault() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index 806b31b9e..e7f8f9a68 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.inflate import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.DrawerSwipeCloseListener @@ -360,6 +361,9 @@ class LibraryController( R.id.action_edit_categories -> { router.pushController(CategoryController().withFadeTransaction()) } + R.id.action_source_migration -> { + router.pushController(MigrationController().withFadeTransaction()) + } else -> return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt new file mode 100644 index 000000000..0fb2c24f8 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaAdapter.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible + +class MangaAdapter(controller: MigrationController) : + FlexibleAdapter>(null, controller) { + + private var items: List>? = null + + override fun updateDataSet(items: MutableList>?) { + if (this.items !== items) { + this.items = items + super.updateDataSet(items) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt new file mode 100644 index 000000000..c0fd058cd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaHolder.kt @@ -0,0 +1,36 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import com.bumptech.glide.load.engine.DiskCacheStrategy +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.data.glide.GlideApp +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import kotlinx.android.synthetic.main.catalogue_list_item.* + +class MangaHolder( + private val view: View, + private val adapter: FlexibleAdapter<*> +) : BaseFlexibleViewHolder(view, adapter) { + + fun bind(item: MangaItem) { + // Update the title of the manga. + title.text = item.manga.title + + // Create thumbnail onclick to simulate long click + thumbnail.setOnClickListener { + // Simulate long click on this view to enter selection mode + onLongClick(itemView) + } + + // Update the cover. + GlideApp.with(itemView.context).clear(thumbnail) + GlideApp.with(itemView.context) + .load(item.manga) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .circleCrop() + .dontAnimate() + .into(thumbnail) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt new file mode 100644 index 000000000..b8f3602c1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MangaItem.kt @@ -0,0 +1,37 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractFlexibleItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga + +class MangaItem(val manga: Manga) : AbstractFlexibleItem() { + + override fun getLayoutRes(): Int { + return R.layout.catalogue_list_item + } + + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): MangaHolder { + return MangaHolder(view, adapter) + } + + override fun bindViewHolder(adapter: FlexibleAdapter<*>, + holder: MangaHolder, + position: Int, + payloads: List?) { + + holder.bind(this) + } + + override fun equals(other: Any?): Boolean { + if (other is MangaItem) { + return manga.id == other.manga.id + } + return false + } + + override fun hashCode(): Int { + return manga.id!!.hashCode() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt new file mode 100644 index 000000000..a72bcc8e5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationController.kt @@ -0,0 +1,135 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.app.Dialog +import android.os.Bundle +import android.support.v7.widget.LinearLayoutManager +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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.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 kotlinx.android.synthetic.main.migration_controller.* + +class MigrationController : NucleusController(), + FlexibleAdapter.OnItemClickListener, + SourceAdapter.OnSelectClickListener { + + private var adapter: FlexibleAdapter>? = null + + private var title: String? = null + set(value) { + field = value + setTitle() + } + + override fun createPresenter(): MigrationPresenter { + return MigrationPresenter() + } + + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { + return inflater.inflate(R.layout.migration_controller, container, false) + } + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + adapter = FlexibleAdapter(null, this) + migration_recycler.layoutManager = LinearLayoutManager(view.context) + migration_recycler.adapter = adapter + } + + override fun onDestroyView(view: View) { + adapter = null + super.onDestroyView(view) + } + + override fun getTitle(): String? { + return title + } + + override fun handleBack(): Boolean { + return if (presenter.state.selectedSource != null) { + presenter.deselectSource() + true + } else { + super.handleBack() + } + } + + fun render(state: ViewState) { + if (state.selectedSource == null) { + title = resources?.getString(R.string.label_migration) + if (adapter !is SourceAdapter) { + adapter = SourceAdapter(this) + migration_recycler.adapter = adapter + } + adapter?.updateDataSet(state.sourcesWithManga) + } else { + title = state.selectedSource.toString() + if (adapter !is MangaAdapter) { + adapter = MangaAdapter(this) + migration_recycler.adapter = adapter + } + adapter?.updateDataSet(state.mangaForSource) + } + } + + fun renderIsReplacingManga(state: ViewState) { + if (state.isReplacingManga) { + if (router.getControllerWithTag(LOADING_DIALOG_TAG) == null) { + LoadingController().showDialog(router, LOADING_DIALOG_TAG) + } + } else { + router.popControllerWithTag(LOADING_DIALOG_TAG) + } + } + + override fun onItemClick(position: Int): Boolean { + val item = adapter?.getItem(position) ?: return false + + if (item is MangaItem) { + val controller = SearchController(item.manga) + controller.targetController = this + + router.pushController(controller.withFadeTransaction()) + } else if (item is SourceItem) { + presenter.setSelectedSource(item.source) + } + return false + } + + override fun onSelectClick(position: Int) { + onItemClick(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) + } + + class LoadingController : DialogController() { + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialDialog.Builder(activity!!) + .progress(true, 0) + .content(R.string.migrating) + .cancelable(false) + .build() + } + } + + companion object { + const val LOADING_DIALOG_TAG = "LoadingDialog" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt new file mode 100644 index 000000000..419e571a6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/MigrationPresenter.kt @@ -0,0 +1,140 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.os.Bundle +import com.jakewharton.rxrelay.BehaviorRelay +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.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.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.combineLatest +import eu.kanade.tachiyomi.util.syncChaptersWithSource +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class MigrationPresenter( + private val sourceManager: SourceManager = Injekt.get(), + private val db: DatabaseHelper = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() +) : BasePresenter() { + + var state = ViewState() + private set(value) { + field = value + stateRelay.call(value) + } + + private val stateRelay = BehaviorRelay.create(state) + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + db.getLibraryMangas() + .asRxObservable() + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { state = state.copy(sourcesWithManga = findSourcesWithManga(it)) } + .combineLatest(stateRelay.map { it.selectedSource } + .distinctUntilChanged(), + { library, source -> library to source }) + .filter { (_, source) -> source != null } + .observeOn(Schedulers.io()) + .map { (library, source) -> libraryToMigrationItem(library, source!!.id) } + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { state = state.copy(mangaForSource = it) } + .subscribe() + + stateRelay + // Render the view when any field other than isReplacingManga changes + .distinctUntilChanged { t1, t2 -> t1.isReplacingManga != t2.isReplacingManga } + .subscribeLatestCache(MigrationController::render) + + stateRelay.distinctUntilChanged { state -> state.isReplacingManga } + .subscribeLatestCache(MigrationController::renderIsReplacingManga) + } + + fun setSelectedSource(source: Source) { + state = state.copy(selectedSource = source, mangaForSource = emptyList()) + } + + fun deselectSource() { + state = state.copy(selectedSource = null, mangaForSource = emptyList()) + } + + private fun findSourcesWithManga(library: List): List { + val header = SelectionHeader() + return library.map { it.source }.toSet() + .mapNotNull { if (it != LocalSource.ID) sourceManager.get(it) else null } + .map { SourceItem(it, header) } + } + + private fun libraryToMigrationItem(library: List, sourceId: Long): List { + return library.filter { it.source == sourceId }.map(::MangaItem) + } + + 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) } + .doOnNext { migrateMangaInternal(source, it, prevManga, manga, replace) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnUnsubscribe { state = state.copy(isReplacingManga = false) } + .subscribe() + } + + private fun migrateMangaInternal(source: Source, sourceChapters: List, + prevManga: Manga, manga: Manga, replace: Boolean) { + + db.inTransaction { + // Update chapters read + if (preferences.migrateChapters().getOrDefault()) { + syncChaptersWithSource(db, sourceChapters, manga, source) + + 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 (preferences.migrateCategories().getOrDefault()) { + val categories = db.getCategoriesForManga(prevManga).executeAsBlocking() + val mangaCategories = categories.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mangaCategories, listOf(manga)) + } + // Update track + if (preferences.migrateTracks().getOrDefault()) { + 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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt new file mode 100644 index 000000000..5cde04ccd --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchController.kt @@ -0,0 +1,108 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.app.Dialog +import android.os.Bundle +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.R +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.catalogue.global_search.CatalogueSearchController +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter +import uy.kohesive.injekt.injectLazy + +class SearchController( + private var manga: Manga? = null +) : CatalogueSearchController(manga?.title) { + + private var newManga: Manga? = null + + override fun createPresenter(): CatalogueSearchPresenter { + return SearchPresenter(initialQuery, manga!!) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putSerializable(::manga.name, manga) + outState.putSerializable(::newManga.name, newManga) + super.onSaveInstanceState(outState) + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + manga = savedInstanceState.getSerializable(::manga.name) as? Manga + newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga + } + + fun migrateManga() { + val target = targetController as? MigrationController ?: return + val manga = manga ?: return + val newManga = newManga ?: return + + router.popController(this) + target.migrateManga(manga, newManga) + } + + fun copyManga() { + val target = targetController as? MigrationController ?: return + val manga = manga ?: return + val newManga = newManga ?: return + + router.popController(this) + target.copyManga(manga, newManga) + } + + override fun onMangaClick(manga: Manga) { + newManga = manga + val dialog = MigrationDialog() + dialog.targetController = this + dialog.showDialog(router) + } + + class MigrationDialog : DialogController() { + + private val preferences: PreferencesHelper by injectLazy() + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val optionTitles = arrayOf( + R.string.chapters, + R.string.categories, + R.string.track + ) + + val optionPrefs = arrayOf( + preferences.migrateChapters(), + preferences.migrateCategories(), + preferences.migrateTracks() + ) + + val preselected = optionPrefs.mapIndexedNotNull { index, preference -> + if (preference.getOrDefault()) index else null + } + + return MaterialDialog.Builder(activity!!) + .content(R.string.migration_dialog_what_to_include) + .items(optionTitles.map { resources?.getString(it) }) + .alwaysCallMultiChoiceCallback() + .itemsCallbackMultiChoice(preselected.toTypedArray(), { _, positions, _ -> + // Save current settings for the next time + optionPrefs.forEachIndexed { index, preference -> + preference.set(index in positions) + } + true + }) + .positiveText(R.string.migrate) + .negativeText(R.string.copy) + .neutralText(android.R.string.cancel) + .onPositive { _, _ -> + (targetController as? SearchController)?.migrateManga() + } + .onNegative { _, _ -> + (targetController as? SearchController)?.copyManga() + } + .build() + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt new file mode 100644 index 000000000..09f4ea6b5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SearchPresenter.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.source.CatalogueSource +import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter + +class SearchPresenter( + initialQuery: String? = "", + private val manga: Manga +) : CatalogueSearchPresenter(initialQuery) { + + override fun getEnabledSources(): List { + // Filter out the source of the selected manga + return super.getEnabledSources() + .filterNot { it.id == manga.source } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt new file mode 100644 index 000000000..cb87fcb9e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SelectionHeader.kt @@ -0,0 +1,50 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder +import kotlinx.android.synthetic.main.catalogue_main_controller_card.* + +/** + * Item that contains the selection header. + */ +class SelectionHeader : AbstractHeaderItem() { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): Holder { + return SelectionHeader.Holder(view, adapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: Holder, + position: Int, payloads: List?) { + // Intentionally empty + } + + class Holder(view: View, adapter: FlexibleAdapter<*>) : BaseFlexibleViewHolder(view, adapter) { + init { + title.text = "Please select a source to migrate from" + } + } + + override fun equals(other: Any?): Boolean { + return other is SelectionHeader + } + + override fun hashCode(): Int { + return 0 + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt new file mode 100644 index 000000000..86df353f5 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceAdapter.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.getResourceColor + +/** + * Adapter that holds the catalogue cards. + * + * @param controller instance of [MigrationController]. + */ +class SourceAdapter(val controller: MigrationController) : + FlexibleAdapter>(null, controller, true) { + + val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) + + private var items: List>? = null + + init { + setDisplayHeadersAtStartUp(true) + } + + /** + * Listener for browse item clicks. + */ + val selectClickListener: OnSelectClickListener? = controller + + /** + * Listener which should be called when user clicks select. + */ + interface OnSelectClickListener { + fun onSelectClick(position: Int) + } + + override fun updateDataSet(items: MutableList>?) { + if (this.items !== items) { + this.items = items + super.updateDataSet(items) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt new file mode 100644 index 000000000..fd644e385 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceHolder.kt @@ -0,0 +1,43 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +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.* + +class SourceHolder(view: View, override val adapter: SourceAdapter) : + BaseFlexibleViewHolder(view, adapter), + SlicedHolder { + + override val slice = Slice(card).apply { + setColor(adapter.cardBackground) + } + + override val viewToSlice: View + get() = card + + init { + source_latest.gone() + source_browse.setText(R.string.select) + source_browse.setOnClickListener { + adapter.selectClickListener?.onSelectClick(adapterPosition) + } + } + + fun bind(item: SourceItem) { + val source = item.source + setCardEdges(item) + + // Set source name + title.text = source.name + + // Set circle letter image. + itemView.post { + image.setImageDrawable(image.getRound(source.name.take(1).toUpperCase(),false)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt new file mode 100644 index 000000000..e64aa0a8b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/SourceItem.kt @@ -0,0 +1,41 @@ +package eu.kanade.tachiyomi.ui.migration + +import android.view.View +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractSectionableItem +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.source.Source + +/** + * Item that contains source information. + * + * @param source Instance of [Source] containing source information. + * @param header The header for this item. + */ +data class SourceItem(val source: Source, val header: SelectionHeader? = null) : + AbstractSectionableItem(header) { + + /** + * Returns the layout resource of this item. + */ + override fun getLayoutRes(): Int { + return R.layout.catalogue_main_controller_card_item + } + + /** + * Creates a new view holder for this item. + */ + override fun createViewHolder(view: View, adapter: FlexibleAdapter<*>): SourceHolder { + return SourceHolder(view, adapter as SourceAdapter) + } + + /** + * Binds this item to the given view holder. + */ + override fun bindViewHolder(adapter: FlexibleAdapter<*>, holder: SourceHolder, + position: Int, payloads: List?) { + + holder.bind(this) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt new file mode 100644 index 000000000..7caa5e9ec --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/migration/ViewState.kt @@ -0,0 +1,10 @@ +package eu.kanade.tachiyomi.ui.migration + +import eu.kanade.tachiyomi.source.Source + +data class ViewState( + val selectedSource: Source? = null, + val mangaForSource: List = emptyList(), + val sourcesWithManga: List = emptyList(), + val isReplacingManga: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/res/layout/migration_controller.xml b/app/src/main/res/layout/migration_controller.xml new file mode 100644 index 000000000..643832cd0 --- /dev/null +++ b/app/src/main/res/layout/migration_controller.xml @@ -0,0 +1,6 @@ + + diff --git a/app/src/main/res/menu/library.xml b/app/src/main/res/menu/library.xml index 5e7bf7ad5..6b9867e63 100644 --- a/app/src/main/res/menu/library.xml +++ b/app/src/main/res/menu/library.xml @@ -27,4 +27,9 @@ android:title="@string/action_edit_categories" app:showAsAction="never"/> + + diff --git a/app/src/main/res/menu/migration.xml b/app/src/main/res/menu/migration.xml new file mode 100644 index 000000000..f783ded6f --- /dev/null +++ b/app/src/main/res/menu/migration.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ca36e182..12f8044cf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ Categories Selected: %1$d Backup - + Source migration Settings @@ -390,6 +390,14 @@ %1$s - Ch.%2$s + + Tap to select the source to migrate from + Select data to include + Select + Migrate + Copy + Migrating… + An error occurred while downloading chapters. You can try again in the downloads section