diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt index 9880fac7da..91aedffbbe 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt @@ -8,6 +8,8 @@ import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.HistoryImpl import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory +import eu.kanade.tachiyomi.data.database.tables.ChapterTable +import eu.kanade.tachiyomi.data.database.tables.HistoryTable class MangaChapterHistoryGetResolver : DefaultGetResolver() { companion object { @@ -37,19 +39,24 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver() val manga = mangaGetResolver.mapFromCursor(cursor) // Get chapter object - val chapter = try { chapterResolver.mapFromCursor(cursor) } catch (e: Exception) { - ChapterImpl() } + val chapter = + if (!cursor.isNull(cursor.getColumnIndex(ChapterTable.COL_MANGA_ID))) chapterResolver + .mapFromCursor( + cursor + ) else ChapterImpl() // Get history object - val history = try { historyGetResolver.mapFromCursor(cursor) } catch (e: Exception) { HistoryImpl() } + val history = + if (!cursor.isNull(cursor.getColumnIndex(HistoryTable.COL_ID))) historyGetResolver.mapFromCursor( + cursor + ) else HistoryImpl() // Make certain column conflicts are dealt with if (chapter.id != null) { manga.id = chapter.manga_id manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl")) } - if (history.id != null) - chapter.id = history.chapter_id + if (history.id != null) chapter.id = history.chapter_id // Return result return MangaChapterHistory(manga, chapter, history) 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 36f8ee9e41..6d44a03455 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 @@ -239,6 +239,9 @@ class DownloadManager(val context: Context) { downloader.start() } else if (downloader.queue.isEmpty() && DownloadService.isRunning(context)) { DownloadService.stop(context) + } else if (downloader.queue.isEmpty()) { + DownloadService.callListeners(false) + downloader.stop() } queue.remove(chapters) val chapterDirs = provider.findChapterDirs(chapters, manga, source) + provider.findTempChapterDirs(chapters, manga, source) 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 968c3d6e10..6701d43a09 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 @@ -5,6 +5,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import rx.subjects.PublishSubject +import kotlin.math.roundToInt class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { @@ -25,10 +26,16 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { @Transient private var statusCallback: ((Download) -> Unit)? = null + val pageProgress: Int + get() { + val pages = pages ?: return 0 + return pages.map(Page::progress).sum() + } + val progress: Int get() { val pages = pages ?: return 0 - return pages.map(Page::progress).average().toInt() + return pages.map(Page::progress).average().roundToInt() } fun setStatusSubject(subject: PublishSubject?) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt index b05e9cfd7c..2a871d6095 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueController.kt @@ -28,6 +28,7 @@ import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.extension.SettingsExtensionsController +import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.RootSearchInterface import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController @@ -269,6 +270,12 @@ class CatalogueController : NucleusController(), router.pushController(controller.withFadeTransaction()) } + override fun expandSearch() { + if (showingExtenions) + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + else activity?.toolbar?.menu?.findItem(R.id.action_search)?.expandActionView() + } + /** * Adds items to the options menu. * @@ -276,6 +283,7 @@ class CatalogueController : NucleusController(), * @param inflater used to load the menu xml. */ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + (activity as? MainActivity)?.setDismissIcon(showingExtenions) if (showingExtenions) { // Inflate menu inflater.inflate(R.menu.extension_main, menu) @@ -336,9 +344,6 @@ class CatalogueController : NucleusController(), ).pushChangeHandler(FadeChangeHandler()) ) } - R.id.action_dismiss -> { - ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED - } else -> return super.onOptionsItemSelected(item) } return true diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt index 458ca153c7..f8a7cc32bb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt @@ -8,7 +8,7 @@ import eu.davidea.flexibleadapter.FlexibleAdapter * * @param context the context of the fragment containing this adapter. */ -class DownloadAdapter(controller: DownloadController) : FlexibleAdapter(null, controller, +class DownloadAdapter(controller: DownloadItemListener) : FlexibleAdapter(null, controller, true) { /** @@ -21,6 +21,12 @@ class DownloadAdapter(controller: DownloadController) : FlexibleAdapter() + + private var scope = CoroutineScope(Job() + Dispatchers.Default) + + /** + * Property to get the queue from the download manager. + */ + val downloadQueue: DownloadQueue + get() = downloadManager.queue + + fun getItems() { + scope.launch { + val items = downloadQueue.map(::DownloadItem) + val hasChanged = if (this@DownloadBottomPresenter.items.size != items.size) true + else { + val oldItemsIds = this@DownloadBottomPresenter.items.mapNotNull { + it.download.chapter.id + }.toLongArray() + val newItemsIds = items.mapNotNull { it.download.chapter.id }.toLongArray() + !oldItemsIds.contentEquals(newItemsIds) + } + this@DownloadBottomPresenter.items = items + if (hasChanged) { + withContext(Dispatchers.Main) { sheet.onNextDownloads(items) } + } + } + } + + /** + * Pauses the download queue. + */ + fun pauseDownloads() { + downloadManager.pauseDownloads() + } + + /** + * Clears the download queue. + */ + fun clearQueue() { + downloadManager.clearQueue() + } + + fun reorder(downloads: List) { + downloadManager.reorderQueue(downloads) + } + + fun cancelDownload(download: Download) { + downloadManager.deletePendingDownloads(download) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt new file mode 100644 index 0000000000..be32b0b46a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadBottomSheet.kt @@ -0,0 +1,272 @@ +package eu.kanade.tachiyomi.ui.download + +import android.app.Activity +import android.content.Context +import android.util.AttributeSet +import android.view.Menu +import android.view.MenuItem +import android.widget.LinearLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadService +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.extension.ExtensionDividerItemDecoration +import eu.kanade.tachiyomi.ui.recents.RecentsController +import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets +import eu.kanade.tachiyomi.util.view.updateLayoutParams +import kotlinx.android.synthetic.main.download_bottom_sheet.view.* + +class DownloadBottomSheet @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = + null +) : LinearLayout(context, attrs), + DownloadAdapter.DownloadItemListener { + lateinit var controller: RecentsController + var sheetBehavior: BottomSheetBehavior<*>? = null + + /** + * Adapter containing the active downloads. + */ + private var adapter: DownloadAdapter? = null + + private val presenter = DownloadBottomPresenter(this) + + /** + * Whether the download queue is running or not. + */ + private var isRunning: Boolean = false + private var activity: Activity? = null + + fun onCreate(controller: RecentsController) { + // Initialize adapter, scroll listener and recycler views + adapter = DownloadAdapter(this) + sheetBehavior = BottomSheetBehavior.from(this) + activity = controller.activity + // Create recycler and set adapter. + dl_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + dl_recycler.adapter = adapter + adapter?.isHandleDragEnabled = true + adapter?.isSwipeEnabled = true + dl_recycler.setHasFixedSize(true) + dl_recycler.addItemDecoration(ExtensionDividerItemDecoration(context)) + dl_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + this.controller = controller + updateDLTitle() + + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = context.obtainStyledAttributes(attrsArray) + val headerHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + dl_recycler.doOnApplyWindowInsets { _, windowInsets, _ -> + dl_recycler.updateLayoutParams { + topMargin = windowInsets.systemWindowInsetTop + headerHeight - sheet_layout.height + } + } + sheet_layout.setOnClickListener { + if (sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED) { + sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } else { + sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + update() + setBottomSheet() + + if (sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED && sheetBehavior?.isHideable == true) sheetBehavior?.state = + BottomSheetBehavior.STATE_HIDDEN + } + + fun update() { + presenter.getItems() + onQueueStatusChange(!presenter.downloadManager.isPaused()) + } + + private fun updateDLTitle() { + val extCount = presenter.downloadQueue.firstOrNull() + title_text.text = if (extCount != null) resources.getString( + R.string.downloading_x, extCount.chapter.name + ) + else "" + } + + /** + * Called when the queue's status has changed. Updates the visibility of the buttons. + * + * @param running whether the queue is now running or not. + */ + private fun onQueueStatusChange(running: Boolean) { + val oldRunning = isRunning + isRunning = running + if (oldRunning != running) { + activity?.invalidateOptionsMenu() + + // Check if download queue is empty and update information accordingly. + setInformationView() + } + } + + /** + * Called from the presenter to assign the downloads for the adapter. + * + * @param downloads the downloads from the queue. + */ + fun onNextDownloads(downloads: List) { + activity?.invalidateOptionsMenu() + setInformationView() + adapter?.updateDataSet(downloads) + setBottomSheet() + } + + /** + * Called when the progress of a download changes. + * + * @param download the download whose progress has changed. + */ + fun onUpdateProgress(download: Download) { + getHolder(download)?.notifyProgress() + } + + /** + * Called when a page of a download is downloaded. + * + * @param download the download whose page has been downloaded. + */ + fun onUpdateDownloadedPages(download: Download) { + getHolder(download)?.notifyDownloadedPages() + } + + /** + * Returns the holder for the given download. + * + * @param download the download to find. + * @return the holder of the download or null if it's not bound. + */ + private fun getHolder(download: Download): DownloadHolder? { + return dl_recycler?.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder + } + + /** + * Set information view when queue is empty + */ + private fun setInformationView() { + updateDLTitle() + setBottomSheet() + if (presenter.downloadQueue.isEmpty()) { + empty_view?.show( + R.drawable.ic_file_download_black_128dp, + R.string.nothing_downloading) + } else { + empty_view?.hide() + } + } + + fun prepareMenu(menu: Menu) { + // Set start button visibility. + menu.findItem(R.id.start_queue)?.isVisible = !isRunning && !presenter.downloadQueue.isEmpty() + + // Set pause button visibility. + menu.findItem(R.id.pause_queue)?.isVisible = isRunning && !presenter.downloadQueue.isEmpty() + + // Set clear button visibility. + menu.findItem(R.id.clear_queue)?.isVisible = !presenter.downloadQueue.isEmpty() + + // Set reorder button visibility. + menu.findItem(R.id.reorder)?.isVisible = !presenter.downloadQueue.isEmpty() + } + + fun onOptionsItemSelected(item: MenuItem): Boolean { + val context = activity ?: return false + when (item.itemId) { + R.id.start_queue -> DownloadService.start(context) + R.id.pause_queue -> { + DownloadService.stop(context) + presenter.pauseDownloads() + } + R.id.clear_queue -> { + DownloadService.stop(context) + presenter.clearQueue() + } + R.id.newest, R.id.oldest -> { + val adapter = adapter ?: return false + val items = adapter.currentItems.sortedBy { it.download.chapter.date_upload } + .toMutableList() + if (item.itemId == R.id.newest) + items.reverse() + adapter.updateDataSet(items) + val downloads = items.mapNotNull { it.download } + presenter.reorder(downloads) + } + } + return true + } + + fun dismiss() { + if (sheetBehavior?.isHideable == true) { + sheetBehavior?.state = BottomSheetBehavior.STATE_HIDDEN + } else { + sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED + } + } + + private fun setBottomSheet() { + val hasQueue = presenter.downloadQueue.isNotEmpty() + if (hasQueue) { + sheetBehavior?.skipCollapsed = !hasQueue + if (sheetBehavior?.state == BottomSheetBehavior.STATE_HIDDEN) sheetBehavior?.state = + BottomSheetBehavior.STATE_COLLAPSED + sheetBehavior?.isHideable = !hasQueue + } else { + sheetBehavior?.isHideable = !hasQueue + sheetBehavior?.skipCollapsed = !hasQueue + if (sheetBehavior?.state == BottomSheetBehavior.STATE_COLLAPSED) sheetBehavior?.state = + BottomSheetBehavior.STATE_HIDDEN + } + controller.setPadding(!hasQueue) + } + + /** + * Called when an item is released from a drag. + * + * @param position The position of the released item. + */ + override fun onItemReleased(position: Int) { + val adapter = adapter ?: return + val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download } + presenter.reorder(downloads) + } + + override fun onItemRemoved(position: Int) { + val download = adapter?.getItem(position)?.download ?: return + presenter.cancelDownload(download) + + adapter?.removeItem(position) + val adapter = adapter ?: return + val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download } + presenter.reorder(downloads) + } + + /** + * Called when the menu item of a download is pressed + * + * @param position The position of the item + * @param menuItem The menu Item pressed + */ + override fun onMenuItemClick(position: Int, menuItem: MenuItem) { + when (menuItem.itemId) { + R.id.move_to_top, R.id.move_to_bottom -> { + val items = adapter?.currentItems?.toMutableList() ?: return + val item = items[position] + items.remove(item) + if (menuItem.itemId == R.id.move_to_top) + items.add(0, item) + else + items.add(item) + adapter?.updateDataSet(items) + val downloads = items.mapNotNull { it.download } + presenter.reorder(downloads) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt index ea731e7ee3..6fb021dfe4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt @@ -299,15 +299,9 @@ class DownloadController : NucleusController(), val downloads = items.mapNotNull { it.download } presenter.reorder(downloads) } - R.id.cancel_download -> { - val download = adapter?.getItem(position)?.download ?: return - presenter.cancelDownload(download) - - adapter?.removeItem(position) - val adapter = adapter ?: return - val downloads = (0 until adapter.itemCount).mapNotNull { adapter.getItem(it)?.download } - presenter.reorder(downloads) - } } } + + override fun onItemRemoved(position: Int) { + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt index 0221da53c4..e04b4795dc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt @@ -64,7 +64,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : if (download_progress.max == 1) { download_progress.max = pages.size * 100 } - download_progress.progress = download.totalProgress + download_progress.progress = download.pageProgress } /** @@ -104,4 +104,16 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) : // Finally show the PopupMenu popup.show() } + + override fun getFrontView(): View { + return front_view + } + + override fun getRearRightView(): View { + return right_view + } + + override fun getRearLeftView(): View { + return left_view + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt index 3b1afa2e41..58f8215b72 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt @@ -36,7 +36,7 @@ ExtensionAdapter.OnButtonClickListener, var shouldCallApi = false /** - * Adapter containing the list of manga from the catalogue. + * Adapter containing the list of extensions */ private var adapter: FlexibleAdapter>? = null 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 3f10225d64..fb62a44d9c 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 @@ -34,6 +34,7 @@ import com.google.android.material.snackbar.Snackbar import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadServiceListener import eu.kanade.tachiyomi.data.notification.NotificationReceiver @@ -67,6 +68,8 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.util.Date import java.util.concurrent.TimeUnit @@ -79,6 +82,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { var drawerArrow: DrawerArrowDrawable? = null private set private var searchDrawable: Drawable? = null + private var dismissDrawable: Drawable? = null private var currentGestureDelegate: SwipeGestureInterface? = null private lateinit var gestureDetector: GestureDetectorCompat @@ -129,6 +133,9 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { searchDrawable = ContextCompat.getDrawable( this, R.drawable.ic_search_white_24dp ) + dismissDrawable = ContextCompat.getDrawable( + this, R.drawable.ic_close_white_24dp + ) var continueSwitchingTabs = false bottom_nav.setOnNavigationItemSelectedListener { item -> @@ -152,12 +159,11 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { } else if (currentRoot.tag()?.toIntOrNull() == id) { if (router.backstackSize == 1) { when (id) { - /*R.id.nav_recents -> { - val showRecents = preferences.showRecentUpdates().getOrDefault() - if (!showRecents) setRoot(RecentChaptersController(), id) - else setRoot(RecentlyReadController(), id) - preferences.showRecentUpdates().set(!showRecents) - }*/ + R.id.nav_recents -> { + val controller = + router.getControllerWithTag(id.toString()) as? RecentsController + controller?.toggleDownloads() + } R.id.nav_library -> { val controller = router.getControllerWithTag(id.toString()) as? LibraryController @@ -215,7 +221,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { toolbar.setNavigationOnClickListener { val rootSearchController = router.backstack.lastOrNull()?.controller() if (rootSearchController is RootSearchInterface) { - toolbar.menu.findItem(R.id.action_search)?.expandActionView() + rootSearchController.expandSearch() } else onBackPressed() } @@ -267,6 +273,10 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { setExtensionsBadge() } + fun setDismissIcon(enabled: Boolean) { + toolbar.navigationIcon = if (enabled) dismissDrawable else searchDrawable + } + private fun setNavBarColor(insets: WindowInsets?) { if (insets == null) return window.navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O_MR1) { @@ -309,8 +319,8 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { delay(100) if (Color.alpha(window?.statusBarColor ?: Color.BLACK) >= 255) window?.statusBarColor = getResourceColor( - android.R.attr.statusBarColor - ) + android.R.attr.statusBarColor + ) } super.onSupportActionModeFinished(mode) } @@ -554,17 +564,15 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { } override fun downloadStatusChanged(downloading: Boolean) { - /*val downloadManager = Injekt.get() + val downloadManager = Injekt.get() val hasQueue = downloading || downloadManager.hasQueue() launchUI { if (hasQueue) { - val badge = navigationView?.getOrCreateBadge(R.id.nav_library) ?: return@launchUI - badge.clearNumber() - badge.backgroundColor = getResourceColor(R.attr.badgeColor) + bottom_nav?.getOrCreateBadge(R.id.nav_recents) } else { - navigationView?.removeBadge(R.id.nav_library) + bottom_nav?.removeBadge(R.id.nav_recents) } - }*/ + } } private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { @@ -638,7 +646,12 @@ interface BottomNavBarInterface { fun canChangeTabs(block: () -> Unit): Boolean } -interface RootSearchInterface +interface RootSearchInterface { + fun expandSearch() { + if (this is Controller) activity?.toolbar?.menu?.findItem(R.id.action_search)?.expandActionView() + } +} + interface SpinnerTitleInterface interface OnTouchEventInterface { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt index 3791efba76..101e7153be 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/SearchActivity.kt @@ -16,6 +16,7 @@ class SearchActivity : MainActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + toolbar.navigationIcon = drawerArrow toolbar.setNavigationOnClickListener { popToRoot() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 748b4a1e74..1534c37803 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -347,10 +347,10 @@ class MangaDetailsController : BaseController, android.R.attr.colorBackground ) val backDropColor = - (if (currentNightMode == Configuration.UI_MODE_NIGHT_NO) it?.getLightMutedColor( + (if (currentNightMode == Configuration.UI_MODE_NIGHT_NO) it?.getLightVibrantColor( colorBack ) - else it?.getDarkMutedColor(colorBack)) ?: colorBack + else it?.getDarkVibrantColor(colorBack)) ?: colorBack coverColor = backDropColor (recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder) ?.setBackDrop(backDropColor) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt index 41724c95e9..31260b4cb8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsController.kt @@ -14,6 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar import eu.davidea.flexibleadapter.FlexibleAdapter @@ -29,12 +30,18 @@ import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController +import eu.kanade.tachiyomi.util.system.dpToPx import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener 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.download_bottom_sheet.* import kotlinx.android.synthetic.main.main_activity.* -import kotlinx.android.synthetic.main.recently_read_controller.* +import kotlinx.android.synthetic.main.recents_controller.* +import kotlin.math.abs +import kotlin.math.max /** * Fragment that shows recently read manga. @@ -59,13 +66,17 @@ class RecentsController(bundle: Bundle? = null) : BaseController(bundle), private var recentItems: List? = null private var snack: Snackbar? = null private var lastChapterId: Long? = null + private var showingDownloads = false + var headerHeight = 0 override fun getTitle(): String? { - return resources?.getString(R.string.short_recents) + return if (showingDownloads) + resources?.getString(R.string.label_download_queue) + else resources?.getString(R.string.short_recents) } override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { - return inflater.inflate(R.layout.recently_read_controller, container, false) + return inflater.inflate(R.layout.recents_controller, container, false) } /** @@ -86,16 +97,90 @@ class RecentsController(bundle: Bundle? = null) : BaseController(bundle), adapter.itemTouchHelperCallback.setSwipeFlags( ItemTouchHelper.LEFT ) - scrollViewWith(recycler, skipFirstSnap = true) + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = view.context.obtainStyledAttributes(attrsArray) + val appBarHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + scrollViewWith(recycler, skipFirstSnap = true) { + headerHeight = it.systemWindowInsetTop + appBarHeight + } if (recentItems != null) adapter.updateDataSet(recentItems!!.toList()) presenter.onCreate() + + dl_bottom_sheet.onCreate(this) + + shadow2.alpha = + if (dl_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_COLLAPSED) 0.25f else 0f + shadow.alpha = + if (dl_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 0f + + dl_bottom_sheet.sheetBehavior?.addBottomSheetCallback(object : + BottomSheetBehavior.BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { + shadow2.alpha = (1 - abs(progress)) * 0.25f + shadow.alpha = (1 - abs(progress)) * 0.5f + sheet_layout.alpha = 1 - progress + activity?.appbar?.y = max(activity!!.appbar.y, -headerHeight * (1 - progress)) + val oldShow = showingDownloads + showingDownloads = progress > 0.92f + if (oldShow != showingDownloads) { + setTitle() + activity?.invalidateOptionsMenu() + } + } + + override fun onStateChanged(p0: View, state: Int) { + if (this@RecentsController.view == null) return + if (state == BottomSheetBehavior.STATE_EXPANDED) activity?.appbar?.y = 0f + if (state == BottomSheetBehavior.STATE_EXPANDED || state == BottomSheetBehavior.STATE_COLLAPSED) { + sheet_layout.alpha = + if (state == BottomSheetBehavior.STATE_COLLAPSED) 1f else 0f + showingDownloads = state == BottomSheetBehavior.STATE_EXPANDED + setTitle() + activity?.invalidateOptionsMenu() + } + + if (state == BottomSheetBehavior.STATE_HIDDEN || state == BottomSheetBehavior.STATE_COLLAPSED) { + shadow2.alpha = if (state == BottomSheetBehavior.STATE_COLLAPSED) 0.25f else 0f + shadow.alpha = if (state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 0f + } + + retainViewMode = + if (state == BottomSheetBehavior.STATE_EXPANDED) RetainViewMode.RETAIN_DETACH else RetainViewMode.RELEASE_DETACH + sheet_layout?.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED + sheet_layout?.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED + setPadding(dl_bottom_sheet.sheetBehavior?.isHideable == true) + } + }) + + if (showingDownloads) { + dl_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + setPadding(dl_bottom_sheet.sheetBehavior?.isHideable == true) + } + + override fun handleRootBack(): Boolean { + if (dl_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) { + dl_bottom_sheet.dismiss() + return true + } + return false + } + + fun setPadding(sheetIsHidden: Boolean) { + recycler.updatePaddingRelative(bottom = if (sheetIsHidden) 0 else 20.dpToPx) + recycler.updateLayoutParams { + bottomMargin = if (sheetIsHidden) 0 else 30.dpToPx + } } override fun onActivityResumed(activity: Activity) { super.onActivityResumed(activity) - if (view != null) + if (view != null) { refresh() + dl_bottom_sheet?.update() + } } override fun onDestroy() { @@ -118,6 +203,9 @@ class RecentsController(bundle: Bundle? = null) : BaseController(bundle), fun updateChapterDownload(download: Download) { if (view == null) return + dl_bottom_sheet.update() + dl_bottom_sheet.onUpdateProgress(download) + dl_bottom_sheet.onUpdateDownloadedPages(download) val id = download.chapter.id ?: return val holder = recycler.findViewHolderForItemId(id) as? RecentMangaHolder ?: return holder.notifyStatus(download.status, download.progress) @@ -204,30 +292,38 @@ class RecentsController(bundle: Bundle? = null) : BaseController(bundle), override fun isSearching() = presenter.query.isNotEmpty() override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.recents, menu) - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView - searchView.queryHint = view?.context?.getString(R.string.search_recents) - if (presenter.query.isNotEmpty()) { - searchItem.expandActionView() - searchView.setQuery(presenter.query, true) - searchView.clearFocus() - } - setOnQueryTextChangeListener(searchView) { - if (presenter.query != it) { - presenter.query = it ?: return@setOnQueryTextChangeListener false - refresh() + (activity as? MainActivity)?.setDismissIcon(showingDownloads) + if (showingDownloads) { + inflater.inflate(R.menu.download_queue, menu) + } else { + inflater.inflate(R.menu.recents, menu) + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + searchView.queryHint = view?.context?.getString(R.string.search_recents) + if (presenter.query.isNotEmpty()) { + searchItem.expandActionView() + searchView.setQuery(presenter.query, true) + searchView.clearFocus() + } + setOnQueryTextChangeListener(searchView) { + if (presenter.query != it) { + presenter.query = it ?: return@setOnQueryTextChangeListener false + refresh() + } + true } - true } } + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + if (showingDownloads) dl_bottom_sheet.prepareMenu(menu) + } + override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { super.onChangeStarted(handler, type) if (type.isEnter) { - if (type == ControllerChangeType.POP_EXIT) { - presenter.onCreate() - } + if (type == ControllerChangeType.POP_EXIT) presenter.onCreate() setHasOptionsMenu(true) } else { snack?.dismiss() @@ -235,7 +331,23 @@ class RecentsController(bundle: Bundle? = null) : BaseController(bundle), } } + fun toggleDownloads() { + if (dl_bottom_sheet.sheetBehavior?.isHideable == false) { + if (showingDownloads) dl_bottom_sheet.dismiss() + else dl_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun expandSearch() { + if (showingDownloads) { + dl_bottom_sheet.dismiss() + } else + activity?.toolbar?.menu?.findItem(R.id.action_search)?.expandActionView() + } + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (showingDownloads) + return dl_bottom_sheet.onOptionsItemSelected(item) when (item.itemId) { R.id.action_refresh -> { if (!LibraryUpdateService.isRunning()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt index 5ef3f436f0..b228143ea1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recents/RecentsPresenter.kt @@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.DownloadQueue @@ -22,6 +23,8 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.Calendar import java.util.Date +import java.util.concurrent.TimeUnit +import kotlin.math.abs class RecentsPresenter( val controller: RecentsController, @@ -87,7 +90,13 @@ class RecentsPresenter( if (query.isEmpty()) { val nChaptersItems = pairs.filter { it.first.history.id == null && it.first.chapter.id != null } - .sortedByDescending { it.second.date_upload } + .sortedWith(Comparator> { f1, f2 -> + if (abs(f1.second.date_fetch - f2.second.date_fetch) <= + TimeUnit.HOURS.toMillis(2)) + f2.second.date_upload.compareTo(f1.second.date_upload) + else + f2.second.date_fetch.compareTo(f1.second.date_fetch) + }) .take(4).map { RecentMangaItem( it.first, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 3046abad11..38b2cbd748 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -17,8 +17,6 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.download.DownloadController import eu.kanade.tachiyomi.util.system.getFilePicker import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -33,13 +31,6 @@ class SettingsDownloadController : SettingsController() { override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { titleRes = R.string.pref_category_downloads - preference { - titleRes = R.string.label_download_queue - onClick { - router.pushController(DownloadController().withFadeTransaction()) - } - } - preference { key = Keys.downloadsDirectory titleRes = R.string.pref_download_directory diff --git a/app/src/main/res/layout/download_bottom_sheet.xml b/app/src/main/res/layout/download_bottom_sheet.xml new file mode 100644 index 0000000000..6909af61a9 --- /dev/null +++ b/app/src/main/res/layout/download_bottom_sheet.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/download_item.xml b/app/src/main/res/layout/download_item.xml index 193a74b752..a4697ac1be 100644 --- a/app/src/main/res/layout/download_item.xml +++ b/app/src/main/res/layout/download_item.xml @@ -1,88 +1,127 @@ - + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> - + - + + - + - + + + android:background="?android:attr/colorBackground" + android:paddingTop="@dimen/material_component_lists_padding_above_list"> - + + + + + + + + + - + - + + \ No newline at end of file diff --git a/app/src/main/res/layout/recents_controller.xml b/app/src/main/res/layout/recents_controller.xml new file mode 100644 index 0000000000..539547228a --- /dev/null +++ b/app/src/main/res/layout/recents_controller.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/recents_footer_item.xml b/app/src/main/res/layout/recents_footer_item.xml index 9b610d74ec..7f918868ea 100644 --- a/app/src/main/res/layout/recents_footer_item.xml +++ b/app/src/main/res/layout/recents_footer_item.xml @@ -15,6 +15,7 @@ android:layout_marginTop="6dp" android:layout_marginBottom="12dp" android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" + android:fontFamily="sans-serif-medium" app:layout_constraintStart_toEndOf="@id/arrow" app:layout_constraintTop_toTopOf="parent" android:textColor="?colorAccent" diff --git a/app/src/main/res/menu/download_single.xml b/app/src/main/res/menu/download_single.xml index e1dc2fa65a..fd293cdfd0 100644 --- a/app/src/main/res/menu/download_single.xml +++ b/app/src/main/res/menu/download_single.xml @@ -5,7 +5,4 @@ - - \ No newline at end of file diff --git a/app/src/main/res/menu/extension_main.xml b/app/src/main/res/menu/extension_main.xml index 0a608ef400..8deae1ca3a 100644 --- a/app/src/main/res/menu/extension_main.xml +++ b/app/src/main/res/menu/extension_main.xml @@ -4,7 +4,6 @@ @@ -22,11 +21,4 @@ android:visible="false" android:checkable="true" app:showAsAction="never"/> - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1283f87cbc..d9452991b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,6 +37,7 @@ Extension update available %d extension updates available + Downloading: %1$s 1 in queue %d in queue @@ -708,6 +709,7 @@ No downloads + Nothing currently downloading No recent chapters No recently read manga Your library is empty, add series to your library from the catalogues.