diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index bbd0e0a299..e739430c06 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -294,4 +294,6 @@ class DownloadManager(val context: Context) { } } + fun addListener(listener: DownloadQueue.DownloadListener) = queue.addListener(listener) + fun removeListener(listener: DownloadQueue.DownloadListener) = queue.removeListener(listener) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index 9c2b503e93..4ac514f732 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -18,14 +18,27 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { set(status) { field = status statusSubject?.onNext(this) + statusCallback?.invoke(this) } @Transient private var statusSubject: PublishSubject? = null + @Transient private var statusCallback: ((Download) -> Unit)? = null + + val progress: Int + get() { + val pages = pages ?: return 0 + return pages.map(Page::progress).average().toInt() + } + fun setStatusSubject(subject: PublishSubject?) { statusSubject = subject } + fun setStatusCallback(f: ((Download) -> Unit)?) { + statusCallback = f + } + companion object { const val NOT_DOWNLOADED = 0 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt index 197140d0f8..fa9f857c88 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt @@ -18,9 +18,12 @@ class DownloadQueue( private val updatedRelay = PublishRelay.create() + private val downloadListeners = mutableListOf() + fun addAll(downloads: List) { downloads.forEach { download -> download.setStatusSubject(statusSubject) + download.setStatusCallback(::setPagesFor) download.status = Download.QUEUE } queue.addAll(downloads) @@ -32,6 +35,10 @@ class DownloadQueue( val removed = queue.remove(download) store.remove(download) download.setStatusSubject(null) + download.setStatusCallback(null) + if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) + download.status = Download.NOT_DOWNLOADED + downloadListeners.forEach { it.updateDownload(download) } if (removed) { updatedRelay.call(Unit) } @@ -52,6 +59,10 @@ class DownloadQueue( fun clear() { queue.forEach { download -> download.setStatusSubject(null) + download.setStatusCallback(null) + if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) + download.status = Download.NOT_DOWNLOADED + downloadListeners.forEach { it.updateDownload(download) } } queue.clear() store.clear() @@ -67,6 +78,27 @@ class DownloadQueue( .startWith(Unit) .map { this } + private fun setPagesFor(download: Download) { + if (download.status == Download.DOWNLOADING) { + if (download.pages != null) + for (page in download.pages!!) + page.setStatusCallback { + callListeners(download) + } + downloadListeners.forEach { it.updateDownload(download) } + } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { + setPagesSubject(download.pages, null) + downloadListeners.forEach { it.updateDownload(download) } + } + else { + downloadListeners.forEach { it.updateDownload(download) } + } + } + + private fun callListeners(download: Download) { + downloadListeners.forEach { it.updateDownload(download) } + } + fun getProgressObservable(): Observable { return statusSubject.onBackpressureBuffer() .startWith(getActiveDownloads()) @@ -74,6 +106,7 @@ class DownloadQueue( if (download.status == Download.DOWNLOADING) { val pageStatusSubject = PublishSubject.create() setPagesSubject(download.pages, pageStatusSubject) + downloadListeners.forEach { it.updateDownload(download) } return@flatMap pageStatusSubject .onBackpressureBuffer() .filter { it == Page.READY } @@ -81,6 +114,7 @@ class DownloadQueue( } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { setPagesSubject(download.pages, null) + downloadListeners.forEach { it.updateDownload(download) } } Observable.just(download) } @@ -95,4 +129,16 @@ class DownloadQueue( } } -} + fun addListener(listener: DownloadListener) { + downloadListeners.add(listener) + } + + fun removeListener(listener: DownloadListener) { + downloadListeners.remove(listener) + } + + interface DownloadListener { + fun updateDownload(download: Download) + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt index a0c0b1989e..1fc29ff5af 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/Page.kt @@ -18,12 +18,19 @@ open class Page( set(value) { field = value statusSubject?.onNext(value) + statusCallback?.invoke(this) } @Transient @Volatile var progress: Int = 0 + set(value) { + field = value + statusCallback?.invoke(this) + } @Transient private var statusSubject: Subject? = null + @Transient private var statusCallback: ((Page) -> Unit)? = null + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { progress = if (contentLength > 0) { (100 * bytesRead / contentLength).toInt() @@ -36,6 +43,10 @@ open class Page( this.statusSubject = subject } + fun setStatusCallback(f: ((Page) -> Unit)?) { + statusCallback = f + } + companion object { const val QUEUE = 0 const val LOAD_PAGE = 1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadButton.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadButton.kt index eeb22a494c..64f77d178f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadButton.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadButton.kt @@ -1,5 +1,6 @@ package eu.kanade.tachiyomi.ui.download +import android.animation.ObjectAnimator import android.content.Context import android.graphics.Color import android.util.AttributeSet @@ -26,9 +27,16 @@ class DownloadButton @JvmOverloads constructor(context: Context, attrs: Attribut R.drawable.filled_circle)?.mutate() private val borderCircle = ContextCompat.getDrawable(context, R.drawable.border_circle)?.mutate() + private var isAnimating = false + private var iconAnimation:ObjectAnimator? = null - fun setDownoadStatus(state: Int, progress: Int = 0) { + fun setDownloadStatus(state: Int, progress: Int = 0) { + if (state != Download.DOWNLOADING) { + iconAnimation?.cancel() + download_icon.alpha = 1f + isAnimating = false + } when (state) { Download.NOT_DOWNLOADED -> { download_border.visible() @@ -55,6 +63,15 @@ class DownloadButton @JvmOverloads constructor(context: Context, attrs: Attribut download_border.drawable.setTint(disabledColor) download_progress.progressDrawable?.setTint(downloadedColor) download_icon.drawable.setTint(disabledColor) + if (!isAnimating) { + iconAnimation = ObjectAnimator.ofFloat(download_icon, "alpha", 1f, 0f).apply { + duration = 1000 + repeatCount = ObjectAnimator.INFINITE + repeatMode = ObjectAnimator.REVERSE + } + iconAnimation?.start() + isAnimating = true + } } Download.DOWNLOADED -> { download_progress.gone() 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 062bf8465b..dc486d68b4 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 @@ -86,6 +86,8 @@ open class LibraryController( */ protected var query = "" + var customQuery = "" + /** * Currently selected mangas. */ @@ -256,6 +258,24 @@ open class LibraryController( } } + override fun onChangeEnded( + changeHandler: ControllerChangeHandler, + changeType: ControllerChangeType + ) { + super.onChangeEnded(changeHandler, changeType) + if (changeType.isEnter) { + if (customQuery.isNotEmpty()) { + query = customQuery + ((activity as MainActivity).toolbar.menu.findItem( + R.id.action_search + )?.actionView as? SearchView)?.setQuery( + customQuery, true + ) + } + customQuery = "" + } + } + override fun onActivityResumed(activity: Activity) { super.onActivityResumed(activity) if (observeLater && ::presenter.isInitialized) { @@ -477,15 +497,19 @@ open class LibraryController( menu.findItem(R.id.action_library_filter).icon.mutate() setOnQueryTextChangeListener(searchView) { - query = it ?: "" - searchRelay.call(query) - true + onSearch(it) } searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() }) } - fun search(query:String) { - this.query = query + open fun onSearch(query: String?): Boolean { + this.query = query ?: "" + searchRelay.call(query) + return true + } + + open fun search(query: String) { + this.customQuery = query } override fun handleRootBack(): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt index ae841cf20e..58edc7344e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryListController.kt @@ -5,15 +5,12 @@ import android.os.Bundle import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.PopupMenu -import androidx.appcompat.widget.SearchView import androidx.core.math.MathUtils.clamp import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager @@ -36,10 +33,8 @@ import eu.kanade.tachiyomi.ui.main.SwipeGestureInterface import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.inflate -import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.updatePaddingRelative -import eu.kanade.tachiyomi.widget.AutofitRecyclerView import kotlinx.android.synthetic.main.filter_bottom_sheet.* import kotlinx.android.synthetic.main.library_grid_recycler.* import kotlinx.android.synthetic.main.library_list_controller.* @@ -302,27 +297,21 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), } override fun reattachAdapter() { - if (libraryLayout == 0)recycler.spanCount = 1 + if (libraryLayout == 0) recycler.spanCount = 1 else recycler.columnWidth = (90 + (preferences.gridSize().getOrDefault() * 30)).dpToPx val position = (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() libraryLayout = preferences.libraryLayout().getOrDefault() recycler.adapter = adapter - (recycler as? AutofitRecyclerView)?.spanCount = if (libraryLayout == 0) 1 else mangaPerRow (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(position, 0) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - setOnQueryTextChangeListener(searchView) { - query = it ?: "" - adapter.setFilter(it) - adapter.performFilter() - true - } + override fun onSearch(query: String?): Boolean { + this.query = query ?: "" + adapter.setFilter(query) + adapter.performFilter() + return true } override fun onDestroyActionMode(mode: ActionMode?) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 229cf6fed9..b5ec2149b6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -12,6 +12,7 @@ import android.view.GestureDetector import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.view.WindowManager import android.webkit.WebView import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.graphics.drawable.DrawerArrowDrawable @@ -54,8 +55,10 @@ import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.setting.SettingsMainController import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.launchUI +import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePadding +import eu.kanade.tachiyomi.util.view.visible import kotlinx.android.synthetic.main.main_activity.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -198,18 +201,19 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION*/ updateRecentsIcon() content.viewTreeObserver.addOnGlobalLayoutListener { - /*val heightDiff: Int = content.rootView.height - content.height + val heightDiff: Int = content.rootView.height - content.height if (heightDiff > 200 && window.attributes.softInputMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) { //keyboard is open, hide layout navigationView.gone() - } else if (navigationView.visibility == View.GONE) { + } else if (navigationView.visibility == View.GONE + && window.attributes.softInputMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) { //keyboard is hidden, show layout // use coroutine to delay so the bottom bar doesn't flash on top of the keyboard launchUI { navigationView.visible() } - }*/ + } } supportActionBar?.setDisplayShowCustomEnabled(true) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChaptersSortBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChaptersSortBottomSheet.kt new file mode 100644 index 0000000000..9d2b290c69 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/ChaptersSortBottomSheet.kt @@ -0,0 +1,157 @@ +package eu.kanade.tachiyomi.ui.manga + + +import android.os.Build +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import android.widget.CompoundButton +import android.widget.RadioButton +import android.widget.RadioGroup +import com.f2prateek.rx.preferences.Preference +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +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.util.system.dpToPx +import eu.kanade.tachiyomi.util.view.setBottomEdge +import eu.kanade.tachiyomi.util.view.setEdgeToEdge +import eu.kanade.tachiyomi.util.view.visibleIf +import kotlinx.android.synthetic.main.chapter_sort_bottom_sheet.* +import uy.kohesive.injekt.injectLazy + +class ChaptersSortBottomSheet(private val controller: MangaChaptersController) : BottomSheetDialog + (controller.activity!!, R.style.BottomSheetDialogTheme) { + + val activity = controller.activity!! + + /** + * Preferences helper. + */ + private val preferences by injectLazy() + + private var sheetBehavior: BottomSheetBehavior<*> + + + init { + // Use activity theme for this layout + val view = activity.layoutInflater.inflate(R.layout.chapter_sort_bottom_sheet, null) + setContentView(view) + + sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup) + setEdgeToEdge(activity, bottom_sheet, view, false) + val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + } else 0 + sheetBehavior.peekHeight = 220.dpToPx + height + + sheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { } + + override fun onStateChanged(p0: View, state: Int) { + if (state == BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior.skipCollapsed = true + } + } + }) + + } + + override fun onStart() { + super.onStart() + sheetBehavior.skipCollapsed = true + sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + /** + * Called when the sheet is created. It initializes the listeners and values of the preferences. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initGeneralPreferences() + setBottomEdge(show_bookmark, activity) + close_button.setOnClickListener { + dismiss() + true + } + settings_scroll_view.viewTreeObserver.addOnGlobalLayoutListener { + val isScrollable = + settings_scroll_view!!.height < bottom_sheet.height + + settings_scroll_view.paddingTop + settings_scroll_view.paddingBottom + close_button.visibleIf(isScrollable) + } + } + + + private fun initGeneralPreferences() { + + show_read.isChecked = controller.presenter.onlyRead() + show_unread.isChecked = controller.presenter.onlyUnread() + show_download.isChecked = controller.presenter.onlyDownloaded() + show_bookmark.isChecked = controller.presenter.onlyBookmarked() + + show_all.isChecked = !(show_read.isChecked || show_unread.isChecked || + show_download.isChecked || show_bookmark.isChecked) + + if (controller.presenter.onlyRead()) + //Disable unread filter option if read filter is enabled. + show_unread.isEnabled = false + if (controller.presenter.onlyUnread()) + //Disable read filter option if unread filter is enabled. + show_read.isEnabled = false + + sort_group.check(if (controller.presenter.manga.sortDescending()) R.id.sort_newest else + R.id.sort_oldest) + + show_titles.isChecked = controller.presenter.manga.displayMode == Manga.DISPLAY_NAME + sort_by_source.isChecked = controller.presenter.manga.sorting == Manga.SORTING_SOURCE + + sort_group.setOnCheckedChangeListener { _, checkedId -> + controller.presenter.setSortOrder(checkedId == R.id.sort_oldest) + dismiss() + } + + /*sort_group.bindToPreference(preferences.libraryLayout()) { + controller.reattachAdapter() + if (sheetBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) + dismiss() + } + uniform_grid.bindToPreference(preferences.uniformGrid()) { + controller.reattachAdapter() + } + grid_size_toggle_group.bindToPreference(preferences.gridSize()) { + controller.reattachAdapter() + } + download_badge.bindToPreference(preferences.downloadBadge()) { + controller.presenter.requestDownloadBadgesUpdate() + } + unread_badge_group.bindToPreference(preferences.unreadBadgeType()) { + controller.presenter.requestUnreadBadgesUpdate() + }*/ + } + + /** + * Binds a checkbox or switch view with a boolean preference. + */ + private fun CompoundButton.bindToPreference(pref: Preference, block: () -> Unit) { + isChecked = pref.getOrDefault() + setOnCheckedChangeListener { _, isChecked -> + pref.set(isChecked) + block() + } + } + + /** + * Binds a radio group with a int preference. + */ + private fun RadioGroup.bindToPreference(pref: Preference, block: () -> Unit) { + (getChildAt(pref.getOrDefault()) as RadioButton).isChecked = true + setOnCheckedChangeListener { _, checkedId -> + val index = indexOfChild(findViewById(checkedId)) + pref.set(index) + block() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaChaptersController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaChaptersController.kt index 2ea16fba1e..82a873565d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaChaptersController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaChaptersController.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.manga import android.animation.ValueAnimator +import android.app.Activity import android.content.Intent import android.content.res.Configuration import android.graphics.Color @@ -8,6 +9,7 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.view.Gravity import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater @@ -15,6 +17,7 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.view.ActionMode +import androidx.appcompat.widget.PopupMenu import androidx.core.graphics.ColorUtils import androidx.palette.graphics.Palette import androidx.recyclerview.widget.DividerItemDecoration @@ -31,6 +34,7 @@ import com.google.android.material.snackbar.Snackbar import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaImpl @@ -41,10 +45,13 @@ import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog +import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.SearchActivity import eu.kanade.tachiyomi.ui.manga.MangaController.Companion.FROM_CATALOGUE_EXTRA import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem +import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate @@ -56,14 +63,18 @@ import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updatePaddingRelative import kotlinx.android.synthetic.main.big_manga_controller.* +import kotlinx.android.synthetic.main.big_manga_controller.swipe_refresh import kotlinx.android.synthetic.main.main_activity.* +import kotlinx.android.synthetic.main.manga_info_controller.* import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class MangaChaptersController : BaseController, ActionMode.Callback, FlexibleAdapter.OnItemClickListener, - ChaptersAdapter.MangaHeaderInterface { + FlexibleAdapter.OnItemLongClickListener, + ChaptersAdapter.MangaHeaderInterface, + ChangeMangaCategoriesDialog.Listener { constructor(manga: Manga?, fromCatalogue: Boolean = false, @@ -121,7 +132,6 @@ class MangaChaptersController : BaseController, // Init RecyclerView and adapter adapter = ChaptersAdapter(this, view.context) - //setReadingDrawable() recycler.adapter = adapter recycler.layoutManager = LinearLayoutManager(view.context) @@ -133,9 +143,6 @@ class MangaChaptersController : BaseController, ) recycler.setHasFixedSize(true) adapter?.fastScroller = fast_scroller - /*activity?.controller_container?.updateLayoutParams { - topMargin = 0 - }*/ val attrsArray = intArrayOf(android.R.attr.actionBarSize) val array = view.context.obtainStyledAttributes(attrsArray) val appbarHeight = array.getDimensionPixelSize(0, 0) @@ -144,6 +151,7 @@ class MangaChaptersController : BaseController, recycler.doOnApplyWindowInsets { v, insets, _ -> headerHeight = appbarHeight + insets.systemWindowInsetTop + offset + swipe_refresh.setProgressViewOffset(false, (-40).dpToPx, headerHeight) (recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder) ?.setTopHeight(headerHeight) fast_scroller?.updateLayoutParams { @@ -156,7 +164,7 @@ class MangaChaptersController : BaseController, presenter.onCreate() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - recycler.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY -> + recycler.setOnScrollChangeListener { _, _, _, _, _ -> val atTop = !recycler.canScrollVertically(-1) if ((!atTop && !toolbarIsColored) || (atTop && toolbarIsColored)) { toolbarIsColored = !atTop @@ -173,7 +181,6 @@ class MangaChaptersController : BaseController, ArgbEvaluator(), colorFrom, colorTo ) colorAnimator?.duration = 250 // milliseconds - //colorAnimation.startDelay = 150 colorAnimator?.addUpdateListener { animator -> (activity as MainActivity).toolbar.setBackgroundColor(animator.animatedValue as Int) activity?.window?.statusBarColor = (animator.animatedValue as Int) @@ -184,8 +191,6 @@ class MangaChaptersController : BaseController, } } } - // recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - // recycler.requestApplyInsetsWhenAttached() GlideApp.with(view.context).load(manga) .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) .signature(ObjectKey(MangaImpl.getLastCoverFetch(manga!!.id!!).toString())) @@ -222,7 +227,11 @@ class MangaChaptersController : BaseController, swipe_refresh.setOnRefreshListener { presenter.refreshAll() } - //adapter?.fastScroller = fast_scroller + } + + override fun onActivityResumed(activity: Activity) { + super.onActivityResumed(activity) + presenter.fetchChapters() } fun showError(message: String) { @@ -230,43 +239,21 @@ class MangaChaptersController : BaseController, view?.snack(message) } + fun updateChapterDownload(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status, presenter.isLockedFromSearch, + download.progress) + } + + private fun getHolder(chapter: Chapter): ChapterMatHolder? { + return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterMatHolder + } + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { super.onChangeStarted(handler, type) if (type == ControllerChangeType.PUSH_ENTER || type == ControllerChangeType.POP_ENTER) { (activity as MainActivity).appbar.setBackgroundColor(Color.TRANSPARENT) (activity as MainActivity).toolbar.setBackgroundColor(Color.TRANSPARENT) activity?.window?.statusBarColor = Color.TRANSPARENT - /* val colorFrom = ((activity as MainActivity).toolbar.background as ColorDrawable).color - val colorTo = Color.TRANSPARENT - colorAnimator = ValueAnimator.ofObject( - ArgbEvaluator(), colorFrom, colorTo) - colorAnimator?.duration = 250 // milliseconds - //colorAnimation.startDelay = 150 - colorAnimator?.addUpdateListener { animator -> - (activity as MainActivity).toolbar.setBackgroundColor(animator.animatedValue as Int) - //activity?.window?.statusBarColor = (animator.animatedValue as Int) - } - colorAnimator?.start()*/ - - /*activity!!.window.setFlags( - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, - WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val insetTop = activity!!.window.decorView.rootWindowInsets.systemWindowInsetTop - val insetBottom = activity!!.window.decorView.rootWindowInsets.stableInsetBottom - (activity)?.appbar?.updateLayoutParams { - topMargin = insetTop - } - - (activity)?.navigationView?.updateLayoutParams { - bottomMargin = insetBottom - } - }*/ - - // - //(activity as MainActivity).toolbar.setBackgroundColor(Color.TRANSPARENT) - //(activity as MainActivity).appbar.gone() } else if (type == ControllerChangeType.PUSH_EXIT || type == ControllerChangeType.POP_EXIT) { colorAnimator?.cancel() @@ -278,22 +265,6 @@ class MangaChaptersController : BaseController, activity?.window?.statusBarColor = activity?.getResourceColor( android.R.attr.colorPrimary ) ?: Color.BLACK - // activity!!.window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) - - /* activity?.window?.statusBarColor = activity?.getResourceColor( - android.R.attr.colorPrimary - ) ?: Color.BLACK*/ - /*(activity as MainActivity).appbar.updateLayoutParams { - topMargin = 0 - } - (activity as MainActivity).navigationView.updateLayoutParams { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - bottomMargin = 0 - } - }*/ - //(activity as MainActivity).appbar.background = null -// (activity as AppCompatActivity).supportActionBar?.show() } } @@ -305,9 +276,12 @@ class MangaChaptersController : BaseController, if (presenter.chapters.isEmpty()) { adapter?.updateDataSet(listOf(ChapterItem(Chapter.createH(), presenter.manga))) } - else - adapter?.updateDataSet(listOf(ChapterItem(Chapter.createH(), presenter.manga)) - + presenter.chapters) + else { + swipe_refresh.isRefreshing = false + adapter?.updateDataSet( + listOf(ChapterItem(Chapter.createH(), presenter.manga)) + presenter.chapters + ) + } } @@ -333,7 +307,62 @@ class MangaChaptersController : BaseController, //} } - fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { + override fun onItemLongClick(position: Int) { + val adapter = adapter ?: return + val item = adapter.getItem(position) ?: return + val itemView = getHolder(item)?.itemView ?: return + val popup = PopupMenu(itemView.context, itemView, Gravity.END) + + // Inflate our menu resource into the PopupMenu's Menu + popup.menuInflater.inflate(R.menu.chapters_mat_single, popup.menu) + + // Hide bookmark if bookmark + popup.menu.findItem(R.id.action_bookmark).isVisible = !item.bookmark + popup.menu.findItem(R.id.action_remove_bookmark).isVisible = item.bookmark + + // Hide mark as unread when the chapter is unread + if (!item.read && item.last_page_read == 0) { + popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false + } + + // Hide mark as read when the chapter is read + if (item.read) { + popup.menu.findItem(R.id.action_mark_as_read).isVisible = false + } + + // Set a listener so we are notified if a menu item is clicked + popup.setOnMenuItemClickListener { menuItem -> + val chapters = listOf(item) + when (menuItem.itemId) { + R.id.action_bookmark -> bookmarkChapters(chapters, true) + R.id.action_remove_bookmark -> bookmarkChapters(chapters, false) + R.id.action_mark_as_read -> markAsRead(chapters) + R.id.action_mark_as_unread -> markAsUnread(chapters) + } + true + } + + // Finally show the PopupMenu + popup.show() + } + + private fun bookmarkChapters(chapters: List, bookmarked: Boolean) { + //destroyActionModeIfNeeded() + presenter.bookmarkChapters(chapters, bookmarked) + } + + private fun markAsRead(chapters: List) { + presenter.markChaptersRead(chapters, true) + if (presenter.preferences.removeAfterMarkedAsRead()) { + presenter.deleteChapters(chapters) + } + } + + private fun markAsUnread(chapters: List) { + presenter.markChaptersRead(chapters, false) + } + + private fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) { val activity = activity ?: return val intent = ReaderActivity.newIntent(activity, manga!!, chapter) if (hasAnimation) { @@ -342,13 +371,10 @@ class MangaChaptersController : BaseController, startActivity(intent) } - fun getStatusBarHeight(): Int { - var result = 0 - val resourceId = resources!!.getIdentifier("status_bar_height", "dimen", "android") - if (resourceId > 0) { - result = resources!!.getDimensionPixelSize(resourceId) - } - return result + override fun onDestroyView(view: View) { + snack?.dismiss() + presenter.onDestroy() + super.onDestroyView(view) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -387,8 +413,6 @@ class MangaChaptersController : BaseController, override fun topCoverHeight(): Int = headerHeight override fun nextChapter(): Chapter? = presenter.getNextUnreadChapter() - override fun newestChapterDate(): Long? = presenter.getNewestChapterTime() - override fun lastChapter(): Float? = presenter.getLatestChapter() override fun mangaSource(): Source = presenter.source override fun readNextChapter() { @@ -421,4 +445,100 @@ class MangaChaptersController : BaseController, } else presenter.downloadChapters(listOf(chapter)) } + + override fun tagClicked(text: String) { + val firstController = router.backstack.first()?.controller() + if (firstController is LibraryController && router.backstack.size == 2) { + router.handleBack() + firstController.search(text) + } + } + + override fun showChapterFilter() { + ChaptersSortBottomSheet(this).show() + } + + override fun chapterCount():Int = presenter.chapters.size + + override fun favoriteManga(longPress: Boolean) { + val manga = presenter.manga + if (longPress) { + if (!manga.favorite) { + presenter.toggleFavorite() + showAddedSnack() + } + val categories = presenter.getCategories() + if (categories.isEmpty()) { + // no categories exist, display a message about adding categories + snack = view?.snack(R.string.action_add_category) + } else { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) + .showDialog(router) + } + } + else { + if (presenter.toggleFavorite()) { + val categories = presenter.getCategories() + val defaultCategoryId = presenter.preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId } + when { + defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory) + defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category + presenter.moveMangaToCategory(manga, null) + else -> { + val ids = presenter.getMangaCategoryIds(manga) + val preselected = ids.mapNotNull { id -> + categories.indexOfFirst { it.id == id }.takeIf { it != -1 } + }.toTypedArray() + + ChangeMangaCategoriesDialog( + this, + listOf(manga), + categories, + preselected + ).showDialog(router) + } + } + showAddedSnack() + } else { + showRemovedSnack() + } + } + } + + private fun showAddedSnack() { + val view = view ?: return + snack?.dismiss() + snack = view.snack(view.context.getString(R.string.manga_added_library)) + } + + private fun showRemovedSnack() { + val view = view ?: return + snack?.dismiss() + snack = view.snack( + view.context.getString(R.string.manga_removed_library), + Snackbar.LENGTH_INDEFINITE + ) { + setAction(R.string.action_undo) { + presenter.setFavorite(true) + } + addCallback(object : BaseTransientBottomBar.BaseCallback() { + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + super.onDismissed(transientBottomBar, event) + if (!presenter.manga.favorite) presenter.confirmDeletion() + } + }) + } + (activity as? MainActivity)?.setUndoSnackBar(snack, fab_favorite) + } + + override fun updateCategoriesForMangas(mangas: List, categories: List) { + val manga = mangas.firstOrNull() ?: return + presenter.moveMangaToCategories(manga, categories) + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt index c3c34a0bfe..65ffea4d0f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.manga import android.content.res.ColorStateList import android.graphics.Color -import android.text.format.DateUtils import android.view.View import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat @@ -17,10 +16,11 @@ import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter import eu.kanade.tachiyomi.util.system.getResourceColor +import eu.kanade.tachiyomi.util.view.gone import eu.kanade.tachiyomi.util.view.updateLayoutParams +import eu.kanade.tachiyomi.util.view.visible import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.manga_header_item.* -import java.util.Date import java.util.Locale class MangaHeaderHolder( @@ -33,17 +33,60 @@ class MangaHeaderHolder( top_view.updateLayoutParams { topMargin = adapter.coverListener?.topCoverHeight() ?: 0 } + more_button.setOnClickListener { expandDesc() } + manga_summary.setOnClickListener { expandDesc() } + less_button.setOnClickListener { + manga_summary.maxLines = 3 + manga_genres_tags.gone() + less_button.gone() + more_button_group.visible() + } + manga_genres_tags.setOnTagClickListener { + adapter.coverListener?.tagClicked(it) + } + filter_button.setOnClickListener { + adapter.coverListener?.showChapterFilter() + } + favorite_button.setOnClickListener { + adapter.coverListener?.favoriteManga(false) + } + favorite_button.setOnLongClickListener { + adapter.coverListener?.favoriteManga(true) + true + } + } + + private fun expandDesc() { + if (more_button.visibility == View.VISIBLE) { + manga_summary.maxLines = Integer.MAX_VALUE + manga_genres_tags.visible() + less_button.visible() + more_button_group.gone() + } } override fun bind(item: ChapterItem, manga: Manga) { manga_full_title.text = manga.currentTitle() + + if (manga.currentGenres().isNullOrBlank().not()) + manga_genres_tags.setTags(manga.currentGenres()?.split(", ")?.map(String::trim)) + else + manga_genres_tags.setTags(emptyList()) + if (manga.currentAuthor() == manga.currentArtist() || manga.currentArtist().isNullOrBlank()) manga_author.text = manga.currentAuthor() else { manga_author.text = "${manga.currentAuthor()?.trim()}, ${manga.currentArtist()}" } - manga_summary.text = manga.currentDesc() + manga_summary.text = manga.currentDesc() ?: itemView.context.getString(R.string + .no_description) + + manga_summary.post { + if (manga_summary.lineCount < 3 && manga.currentGenres().isNullOrBlank()) { + more_button_group.gone() + } + } manga_summary_label.text = itemView.context.getString(R.string.about_this, itemView.context.getString( when { @@ -77,6 +120,10 @@ class MangaHeaderHolder( context.getResourceColor(R.attr.colorAccent), 75)) strokeColor = ColorStateList.valueOf(Color.TRANSPARENT) } + else strokeColor = ColorStateList.valueOf( + ColorUtils.setAlphaComponent( + itemView.context.getResourceColor(R.attr + .colorOnSurface), 31)) } true_backdrop.setBackgroundColor(adapter.coverListener?.coverColor() ?: itemView.context.getResourceColor(android.R.attr.colorBackground)) @@ -98,31 +145,20 @@ class MangaHeaderHolder( } } + val count = adapter.coverListener?.chapterCount() ?: 0 + chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count) + top_view.updateLayoutParams { topMargin = adapter.coverListener?.topCoverHeight() ?: 0 } - val lastUpdated = adapter.coverListener?.newestChapterDate() - if (lastUpdated != null) { - manga_last_update.text = itemView.context.getString( - R.string.last_updated, DateUtils.getRelativeTimeSpanString( - lastUpdated, Date().time, DateUtils.HOUR_IN_MILLIS - ).toString() - ) - } - else { - manga_last_update.text = itemView.context.getString(R.string.last_update_unknown) - } - val sourceAndStatus = mutableListOf() - sourceAndStatus.add(itemView.context.getString( when (manga.status) { + manga_status.text = (itemView.context.getString( when (manga.status) { SManga.ONGOING -> R.string.ongoing SManga.COMPLETED -> R.string.completed SManga.LICENSED -> R.string.licensed else -> R.string.unknown_status })) - val sourceName = adapter.coverListener?.mangaSource()?.toString() - if (sourceName != null) sourceAndStatus.add(sourceName) - manga_status_source.text = sourceAndStatus.joinToString(" • ") + manga_source.text = adapter.coverListener?.mangaSource()?.toString() GlideApp.with(view.context).load(manga) .diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 1ab5db7620..72250e4533 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -1,46 +1,65 @@ package eu.kanade.tachiyomi.ui.manga +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Date +import kotlin.coroutines.CoroutineContext class MangaPresenter(private val controller: MangaChaptersController, val manga: Manga, val source: Source, val preferences: PreferencesHelper = Injekt.get(), + private val coverCache: CoverCache = Injekt.get(), private val db: DatabaseHelper = Injekt.get(), - private val downloadManager: DownloadManager = Injekt.get()) { + private val downloadManager: DownloadManager = Injekt.get()): + CoroutineScope, + DownloadQueue.DownloadListener { + override var coroutineContext:CoroutineContext = Job() + Dispatchers.Default var isLockedFromSearch = false var hasRequested = false var chapters:List = emptyList() private set + fun onCreate() { isLockedFromSearch = SecureActivityDelegate.shouldBeLocked() + downloadManager.addListener(this) if (!manga.initialized) fetchMangaFromSource() updateChapters() controller.updateChapters(this.chapters) } + fun onDestroy() { + downloadManager.removeListener(this) + } + fun fetchMangaFromSource() { GlobalScope.launch(Dispatchers.IO) { withContext(Dispatchers.Main) { @@ -66,6 +85,24 @@ class MangaPresenter(private val controller: MangaChaptersController, } } + fun fetchChapters() { + launch { + getChapters() + withContext(Dispatchers.Main) { controller.updateChapters(chapters) } + } + } + + private suspend fun getChapters() { + val chapters = withContext(Dispatchers.IO) { + db.getChapters(manga).executeAsBlocking().map { it.toModel() } + } + // Store the last emission + this.chapters = applyChapterFilters(chapters) + + // Find downloaded chapters + setDownloadedChapters(chapters) + } + private fun updateChapters(fetchedChapters: List? = null) { val chapters = (fetchedChapters ?: db.getChapters(manga).executeAsBlocking()).map { it.toModel() } @@ -75,17 +112,6 @@ class MangaPresenter(private val controller: MangaChaptersController, // Find downloaded chapters setDownloadedChapters(chapters) - - /* - // Emit the number of chapters to the info tab. - chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number - ?: 0f) - - // Emit the upload date of the most recent chapter - lastUpdateRelay.call( - Date(chapters.maxBy { it.date_upload }?.date_upload - ?: 0) - )*/ } /** @@ -100,6 +126,14 @@ class MangaPresenter(private val controller: MangaChaptersController, } } } + + override fun updateDownload(download: Download) { + chapters.find { it.id == download.chapter.id }?.download = download + launch(Dispatchers.Main) { + controller.updateChapterDownload(download) + } + } + /** * Converts a chapter from the database to an extended model, allowing to store new fields. */ @@ -237,9 +271,11 @@ class MangaPresenter(private val controller: MangaChaptersController, * @param chapters the list of chapters to delete. */ fun deleteChapters(chapters: List) { - deleteChaptersInternal(chapters) + deleteChaptersInternal(chapters) - setDownloadedChapters(chapters) + chapters.forEach { chapter -> + this.chapters.find { it.id == chapter.id }?.download?.status = Download.NOT_DOWNLOADED + } controller.updateChapters(this.chapters) // if (onlyDownloaded()) refreshChapters() } @@ -258,8 +294,46 @@ class MangaPresenter(private val controller: MangaChaptersController, } fun refreshAll() { - fetchMangaFromSource() - fetchChaptersFromSource() + launch { + var mangaError: java.lang.Exception? = null + var chapterError: java.lang.Exception? = null + val chapters = async(Dispatchers.IO) { + try { + source.fetchChapterList(manga).toBlocking().single() + + } catch (e: Exception) { + chapterError = e + emptyList() + } ?: emptyList() + } + val thumbnailUrl = manga.thumbnail_url + val nManga = async(Dispatchers.IO) { + try { + source.fetchMangaDetails(manga).toBlocking().single() + } catch (e: java.lang.Exception) { + mangaError = e + null + } + } + + val networkManga = nManga.await() + if (networkManga != null) { + manga.copyFrom(networkManga) + manga.initialized = true + db.insertManga(manga).executeAsBlocking() + if (thumbnailUrl != networkManga.thumbnail_url) + MangaImpl.setLastCoverFetch(manga.id!!, Date().time) + } + val finChapters = chapters.await() + if (finChapters.isNotEmpty()) { + syncChaptersWithSource(db, finChapters, manga, source) + withContext(Dispatchers.IO) { updateChapters() } + } + if (chapterError == null) + withContext(Dispatchers.Main) { controller.updateChapters(this@MangaPresenter.chapters) } + if (mangaError != null) + withContext(Dispatchers.Main) { controller.showError(trimException(mangaError!!)) } + } } /** @@ -268,12 +342,12 @@ class MangaPresenter(private val controller: MangaChaptersController, fun fetchChaptersFromSource() { hasRequested = true - GlobalScope.launch(Dispatchers.IO) { + launch(Dispatchers.IO) { val chapters = try { source.fetchChapterList(manga).toBlocking().single() } catch(e: Exception) { - controller.showError(trimException(e)) + withContext(Dispatchers.Main) { controller.showError(trimException(e)) } return@launch } ?: listOf() try { @@ -291,4 +365,108 @@ class MangaPresenter(private val controller: MangaChaptersController, private fun trimException(e: java.lang.Exception): String { return e.message?.split(": ")?.drop(1)?.joinToString(": ") ?: "Error" } + + /** + * Bookmarks the given list of chapters. + * @param selectedChapters the list of chapters to bookmark. + */ + fun bookmarkChapters(selectedChapters: List, bookmarked: Boolean) { + launch(Dispatchers.IO) { + selectedChapters.forEach { + it.bookmark = bookmarked + } + db.updateChaptersProgress(selectedChapters).executeAsBlocking() + withContext(Dispatchers.Main) { controller.updateChapters(chapters) } + } + } + + /** + * Mark the selected chapter list as read/unread. + * @param selectedChapters the list of selected chapters. + * @param read whether to mark chapters as read or unread. + */ + fun markChaptersRead(selectedChapters: List, read: Boolean) { + launch(Dispatchers.IO) { + selectedChapters.forEach { + it.read = read + if (!read) { + it.last_page_read = 0 + it.pages_left = 0 + } + } + db.updateChaptersProgress(selectedChapters).executeAsBlocking() + withContext(Dispatchers.Main) { controller.updateChapters(chapters) } + } + } + + /** + * Reverses the sorting and requests an UI update. + */ + fun setSortOrder(desend: Boolean) { + manga.setChapterOrder(if (desend) Manga.SORT_ASC else Manga.SORT_DESC) + db.updateFlags(manga).executeAsBlocking() + updateChapters() + controller.updateChapters(chapters) + } + + fun toggleFavorite(): Boolean { + manga.favorite = !manga.favorite + db.insertManga(manga).executeAsBlocking() + controller.updateHeader() + return manga.favorite + } + + /** + * Get user categories. + * + * @return List of categories, not including the default category + */ + fun getCategories(): List { + return db.getCategories().executeAsBlocking() + } + + /** + * Move the given manga to the category. + * + * @param manga the manga to move. + * @param category the selected category, or null for default category. + */ + fun moveMangaToCategory(manga: Manga, category: Category?) { + moveMangaToCategories(manga, listOfNotNull(category)) + } + + /** + * Move the given manga to categories. + * + * @param manga the manga to move. + * @param categories the selected categories. + */ + fun moveMangaToCategories(manga: Manga, categories: List) { + val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) } + db.setMangaCategories(mc, listOf(manga)) + } + + /** + * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. + * + * @param manga the manga to get categories from. + * @return Array of category ids the manga is in, if none returns default id + */ + fun getMangaCategoryIds(manga: Manga): Array { + val categories = db.getCategoriesForManga(manga).executeAsBlocking() + return categories.mapNotNull { it.id }.toTypedArray() + } + + fun confirmDeletion() { + coverCache.deleteFromCache(manga.thumbnail_url) + db.resetMangaInfo(manga).executeAsBlocking() + downloadManager.deleteManga(manga, source) + } + + fun setFavorite(favorite: Boolean) { + if (manga.favorite == favorite) { + return + } + toggleFavorite() + } } \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterMatHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterMatHolder.kt index bcd8d13df2..6c194b1a2b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterMatHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChapterMatHolder.kt @@ -29,7 +29,9 @@ class ChapterMatHolder( private fun downloadOrRemoveMenu() { val chapter = adapter.getItem(adapterPosition) ?: return - if (chapter.status != Download.NOT_DOWNLOADED) { + if (chapter.status == Download.NOT_DOWNLOADED || chapter.status == Download.ERROR) { + adapter.coverListener?.downloadChapter(adapterPosition) + } else { download_button.post { // Create a PopupMenu, giving it the clicked view for an anchor val popup = PopupMenu(download_button.context, download_button) @@ -38,8 +40,7 @@ class ChapterMatHolder( popup.menuInflater.inflate(R.menu.chapter_download, popup.menu) // Hide download and show delete if the chapter is downloaded - if (chapter.status != Download.DOWNLOADED) popup.menu.findItem(R.id.action_delete) - .title = download_button.context.getString( + if (chapter.status != Download.DOWNLOADED) popup.menu.findItem(R.id.action_delete).title = download_button.context.getString( R.string.action_cancel ) @@ -53,9 +54,6 @@ class ChapterMatHolder( popup.show() } } - else { - adapter.coverListener?.downloadChapter(adapterPosition) - } } override fun bind(item: ChapterItem, manga: Manga) { @@ -113,6 +111,6 @@ class ChapterMatHolder( return } visible() - setDownoadStatus(status, progress) + setDownloadStatus(status, progress) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt index 8d9daf2c10..03f94ab2ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt @@ -64,8 +64,10 @@ class ChaptersAdapter( fun readNextChapter() fun downloadChapter(position: Int) fun topCoverHeight(): Int - fun newestChapterDate(): Long? - fun lastChapter(): Float? + fun chapterCount(): Int + fun tagClicked(text: String) fun mangaSource(): Source + fun showChapterFilter() + fun favoriteManga(longPress: Boolean) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt index 87ba0efea7..c788c526c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoController.kt @@ -386,9 +386,9 @@ class MangaInfoController : NucleusController(), fun setLastUpdateDate(date: Date) { if (date.time != 0L) { - manga_last_update?.text = dateFormat.format(date) + manga_status?.text = dateFormat.format(date) } else { - manga_last_update?.text = resources?.getString(R.string.unknown) + manga_status?.text = resources?.getString(R.string.unknown) } } diff --git a/app/src/main/res/layout-land/manga_info_controller.xml b/app/src/main/res/layout-land/manga_info_controller.xml index fcdf11a0e0..70cb3325c0 100644 --- a/app/src/main/res/layout-land/manga_info_controller.xml +++ b/app/src/main/res/layout-land/manga_info_controller.xml @@ -150,7 +150,7 @@ app:layout_constraintEnd_toEndOf="parent" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chapters_mat_item.xml b/app/src/main/res/layout/chapters_mat_item.xml index 7b37382f49..f7bf6893e7 100644 --- a/app/src/main/res/layout/chapters_mat_item.xml +++ b/app/src/main/res/layout/chapters_mat_item.xml @@ -28,6 +28,7 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:layout_marginBottom="12dp" + android:ellipsize="end" android:maxLines="1" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/download_button" diff --git a/app/src/main/res/layout/manga_header_item.xml b/app/src/main/res/layout/manga_header_item.xml index a1ae92f652..dcac380bb7 100644 --- a/app/src/main/res/layout/manga_header_item.xml +++ b/app/src/main/res/layout/manga_header_item.xml @@ -25,11 +25,11 @@ android:id="@+id/true_backdrop" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintHeight_min="200dp" + app:layout_constraintBottom_toBottomOf="@id/bottom_line" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_min="200dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="@id/bottom_line" app:layout_constraintVertical_bias="0.0" tools:background="@color/material_red_400" /> @@ -39,10 +39,10 @@ android:layout_height="0dp" android:alpha="0.1" android:scaleType="centerCrop" + app:layout_constraintBottom_toBottomOf="@+id/true_backdrop" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/true_backdrop" - app:layout_constraintBottom_toBottomOf="@+id/true_backdrop" tools:src="@mipmap/ic_launcher" /> @@ -89,13 +89,13 @@ android:id="@+id/cover_card" android:layout_width="100dp" android:layout_height="wrap_content" - android:layout_marginBottom="16dp" android:layout_marginStart="16dp" + android:layout_marginBottom="16dp" app:layout_constraintBottom_toBottomOf="@id/guideline" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/true_backdrop" app:layout_constraintEnd_toEndOf="@id/manga_layout" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/top_view" + app:layout_constraintTop_toTopOf="@id/true_backdrop" app:layout_constraintVertical_bias="1.0"> + app:layout_constraintTop_toBottomOf="@+id/manga_author" + tools:text="Completed" /> + app:layout_constraintTop_toBottomOf="@id/manga_status" + tools:text="Mangadex (EN)" /> + app:constraint_referenced_ids="manga_source,manga_layout,cover_card" /> + app:layout_constraintBottom_toBottomOf="@id/chapters_title" /> + + + android:text="@string/more" + android:layout_marginEnd="8dp" + android:textAlignment="textEnd" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/more_guide" + app:rippleColor="@null" /> + + + + - - + app:layout_constraintTop_toBottomOf="@id/start_reading_button" + app:layout_constraintEnd_toStartOf="@id/filters_text"/> + app:layout_constraintTop_toTopOf="@id/chapters_title" + app:layout_constraintEnd_toEndOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/manga_info_controller.xml b/app/src/main/res/layout/manga_info_controller.xml index 9c1e15ac7c..651d2c0c4e 100644 --- a/app/src/main/res/layout/manga_info_controller.xml +++ b/app/src/main/res/layout/manga_info_controller.xml @@ -172,7 +172,7 @@ app:layout_constraintEnd_toEndOf="parent"/> + + + + + + + + + + + + + + \ 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 adebed6d7d..39ae67d696 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,6 +70,7 @@ Mark as read Mark as unread Mark previous as read + Mark multiple Download Bookmark Remove bookmark @@ -224,7 +225,7 @@ Always Never - After %1$s minutes + After %1$s minute After %1$s minutes Search title, tags, source @@ -500,6 +501,11 @@ %1$s copied to clipboard Source not installed: %1$s About this %1$s + + %1$d chapter + %1$d chapters + + No description Start reading @@ -697,5 +703,7 @@ Reset Tags Display as Auto + More + Less diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 12da731451..3cd736afed 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -252,6 +252,12 @@ 0.0 + +