From 1966dd594f8d70245bdd1de646f97c2f0fdc22d0 Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 9 Mar 2020 21:09:02 -0700 Subject: [PATCH 01/20] Added animation to scrolling sideways in library list mode --- .../ui/library/LibraryCategoryAdapter.kt | 1 + .../ui/library/LibraryCategoryView.kt | 1 + .../tachiyomi/ui/library/LibraryHeaderItem.kt | 1 + .../tachiyomi/ui/library/LibraryHolder.kt | 5 + .../ui/library/LibraryListController.kt | 255 +++++++++++++++++- .../kanade/tachiyomi/ui/main/MainActivity.kt | 19 +- .../tachiyomi/util/view/ViewExtensions.kt | 8 +- 7 files changed, 272 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt index 1975ab8037..fca03ba4c0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryAdapter.kt @@ -183,5 +183,6 @@ class LibraryCategoryAdapter(val libraryListener: LibraryListener) : fun selectAll(position: Int) fun allSelected(position: Int): Boolean fun showCategories(position: Int, view: View) + fun recyclerIsScrolling(): Boolean } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt index b46ec61726..114cf40908 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryCategoryView.kt @@ -399,4 +399,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att override fun selectAll(position: Int) { } override fun allSelected(position: Int): Boolean = false override fun showCategories(position: Int, view: View) { } + override fun recyclerIsScrolling() = false } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt index b8063c7e07..b676f946a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHeaderItem.kt @@ -144,6 +144,7 @@ class LibraryHeaderItem(private val categoryF: (Int) -> Category, val catId: Int } } private fun showCatSortOptions() { + if (adapter.libraryListener.recyclerIsScrolling()) return val category = (adapter.getItem(adapterPosition) as? LibraryHeaderItem)?.category ?: return // Create a PopupMenu, giving it the clicked view for an anchor diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt index 3e294a3605..b519c33bef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryHolder.kt @@ -50,4 +50,9 @@ abstract class LibraryHolder( super.onItemReleased(position) (adapter as? LibraryCategoryAdapter)?.libraryListener?.onItemReleased(position) } + + override fun onLongClick(view: View?): Boolean { + super.onLongClick(view) + return !adapter.libraryListener.recyclerIsScrolling() + } } 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 2d40139e2e..5f7e5b44b9 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 @@ -1,14 +1,20 @@ package eu.kanade.tachiyomi.ui.library +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator import android.graphics.Rect +import android.os.Build import android.os.Bundle import android.util.TypedValue import android.view.Gravity import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.PopupMenu +import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.math.MathUtils.clamp import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager @@ -26,6 +32,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.ui.main.OnTouchEventInterface import eu.kanade.tachiyomi.ui.main.SpinnerTitleInterface import eu.kanade.tachiyomi.ui.main.SwipeGestureInterface import eu.kanade.tachiyomi.util.system.dpToPx @@ -33,6 +40,7 @@ import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.view.inflate import eu.kanade.tachiyomi.util.view.scrollViewWith 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.filter_bottom_sheet.* import kotlinx.android.synthetic.main.library_grid_recycler.* @@ -40,8 +48,13 @@ import kotlinx.android.synthetic.main.library_list_controller.* import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.spinner_title.view.* import kotlinx.coroutines.delay +import timber.log.Timber import java.util.Locale +import kotlin.math.abs import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.math.sign class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), FlexibleAdapter.OnItemClickListener, @@ -49,6 +62,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), FlexibleAdapter.OnItemMoveListener, LibraryCategoryAdapter.LibraryListener, SpinnerTitleInterface, + OnTouchEventInterface, SwipeGestureInterface { private lateinit var adapter: LibraryCategoryAdapter @@ -66,6 +80,17 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), private var switchingCategories = false + var startPosX:Float? = null + var startPosY:Float? = null + var moved = false + var lockedRecycler = false + var lockedY = false + var nextCategory:Int? = null + var ogCategory:Int? = null + var prevCategory:Int? = null + private val swipeDistance = 300f + var flinging = false + /** * Recycler view of the list of manga. */ @@ -73,7 +98,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), override fun contentView():View = recycler_layout - /* override fun getTitle(): String? { + override fun getTitle(): String? { return if (::customTitleSpinner.isInitialized) customTitleSpinner.category_title.text.toString() else super.getTitle() // when { @@ -81,7 +106,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), // spinnerAdapter?.array?.size == 1 -> return spinnerAdapter?.array?.firstOrNull() // else -> return super.getTitle() // } - }*/ + } private var scrollListener = object : RecyclerView.OnScrollListener () { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -115,6 +140,156 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), } } + override fun onTouchEvent(event: MotionEvent?) { + if (event == null) { + resetScrollingValues() + resetRecyclerY() + return + } + if (flinging) return + val sheetRect = Rect() + val recyclerRect = Rect() + bottom_sheet.getGlobalVisibleRect(sheetRect) + view?.getGlobalVisibleRect(recyclerRect) + + if (sheetRect.contains(event.x.toInt(), event.y.toInt()) || + !recyclerRect.contains(event.x.toInt(), event.y.toInt())) { + return + } + if (startPosX == null) { + startPosX = event.rawX + startPosY = event.rawY + val position = + (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() + val order = when (val item = adapter.getItem(position)) { + is LibraryHeaderItem -> item.category.order + is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order + else -> null + } + if (order != null) { + ogCategory = order + var newOffsetN = order + 1 + while (adapter.indexOf(newOffsetN) == -1 && presenter.categories.any { it.order == newOffsetN }) { + newOffsetN += 1 + } + if (adapter.indexOf(newOffsetN) != -1) + nextCategory = newOffsetN + + if (position == 0) prevCategory = null + else { + var newOffsetP = order - 1 + while (adapter.indexOf(newOffsetP) == -1 && presenter.categories.any { it.order == newOffsetP }) { + newOffsetP -= 1 + } + if (adapter.indexOf(newOffsetP) != -1) + prevCategory = newOffsetP + } + } + } + else if (event.actionMasked != MotionEvent.ACTION_UP && startPosX != null) { + val distance = abs(event.rawX - startPosX!!) + val sign = sign(event.rawX - startPosX!!) + + if (lockedY) return + + if (distance > 60 && abs(event.rawY - startPosY!!) <= 30 && + !lockedRecycler) { + lockedRecycler = true + switchingCategories = true + if ((prevCategory == null && sign > 0) || (nextCategory == null && sign < 0)) { + recycler_layout.x = 0f + recycler_layout.alpha = 1f + return + } + else + recycler.suppressLayout(true) + } + else if (!lockedRecycler && abs(event.rawY - startPosY!!) > 30) { + lockedY = true + resetRecyclerY() + return + } + if ((prevCategory == null && sign > 0) || (nextCategory == null && sign < 0)) { + resetRecyclerY() + return + } + if (abs(event.rawY - startPosY!!) <= 30 || recycler.isLayoutSuppressed + || lockedRecycler) { + if (distance <= swipeDistance * 1.1f) { + recycler_layout.x = (max(0f, distance - 50f) * sign) / 3 + recycler_layout.alpha = + (1f - (distance - (swipeDistance * 0.1f)) / swipeDistance) + if (moved) { + scrollToHeader(ogCategory ?: -1) + moved = false + } + } else { + if (!moved) { + scrollToHeader((if (sign <= 0) nextCategory else prevCategory) ?: -1) + moved = true + } + recycler_layout.x = ((distance - swipeDistance * 2) * sign) / 3 + recycler_layout.alpha = ((distance - swipeDistance * 1.1f) / swipeDistance) + if (sign > 0) { + recycler_layout.x = min(0f, recycler_layout.x) + } else { + recycler_layout.x = max(0f, recycler_layout.x) + } + recycler_layout.alpha = min(1f, recycler_layout.alpha) + } + } + } + else if (event.actionMasked == MotionEvent.ACTION_UP) { + recycler_layout.post { + if (!flinging) { + resetScrollingValues() + resetRecyclerY(true) + } + } + } + } + + private fun resetScrollingValues() { + startPosX = null + startPosY = null + nextCategory = null + prevCategory = null + ogCategory = null + lockedY = false + } + + private fun resetRecyclerY(animated: Boolean = false, time: Long = 100) { + moved = false + lockedRecycler = false + if (animated) { + val set = AnimatorSet() + val translationXAnimator = ValueAnimator.ofFloat(recycler_layout.x, 0f) + translationXAnimator.duration = time + translationXAnimator.addUpdateListener { + animation -> recycler_layout.x = animation.animatedValue as Float + } + + val translationAlphaAnimator = ValueAnimator.ofFloat(recycler_layout.alpha, 1f) + translationAlphaAnimator.duration = time + translationAlphaAnimator.addUpdateListener { + animation -> recycler_layout.alpha = animation.animatedValue as Float + } + set.playTogether(translationXAnimator, translationAlphaAnimator) + set.start() + + launchUI { + delay(time) + if (!lockedRecycler) switchingCategories = false + } + } + else { + recycler_layout.x = 0f + recycler_layout.alpha = 1f + switchingCategories = false + } + recycler.suppressLayout(false) + } + override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { return inflater.inflate(R.layout.library_list_controller, container, false) } @@ -133,7 +308,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), }) recycler.setHasFixedSize(true) recycler.adapter = adapter - adapter.fastScroller = fast_scroller + //adapter.fastScroller = fast_scroller recycler.addOnScrollListener(scrollListener) val tv = TypedValue() @@ -258,14 +433,26 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), } } - private fun scrollToHeader(pos: Int, fade:Boolean = false) { + private fun scrollToHeader(pos: Int) { val headerPosition = adapter.indexOf(pos) switchingCategories = true if (headerPosition > -1) { - activity?.appbar?.y = 0f + val appbar = activity?.appbar + //if (headerPosition == 0) + //activity?.appbar?.y = 0f recycler.suppressLayout(true) + val appbarOffset = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (appbar?.y ?: 0f > -20) 0 else + (appbar?.y?.plus(view?.rootWindowInsets?.systemWindowInsetTop ?: 0) + ?: 0f).roundToInt() + 10.dpToPx + } + else { + 0 + } (recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( - headerPosition, if (headerPosition == 0) 0 else (-30).dpToPx + headerPosition, (if (headerPosition == 0) 0 else (-28).dpToPx) + + appbarOffset ) recycler.suppressLayout(false) } @@ -349,6 +536,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), } override fun startReading(position: Int) { + if (recyclerIsScrolling()) return if (adapter.mode == SelectableAdapter.Mode.MULTI) { toggleSelection(position) return @@ -381,7 +569,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), * @return true if the item should be selected, false otherwise. */ override fun onItemClick(view: View?, position: Int): Boolean { - if (switchingCategories) return false + if (recyclerIsScrolling()) return false val item = adapter.getItem(position) as? LibraryItem ?: return false return if (adapter.mode == SelectableAdapter.Mode.MULTI) { lastClickPosition = position @@ -399,6 +587,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), * @param position the position of the element clicked. */ override fun onItemLongClick(position: Int) { + if (recyclerIsScrolling()) return createActionModeIfNeeded() when { lastClickPosition == -1 -> setSelection(position) @@ -586,6 +775,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), override fun onSwipeRight(x: Float, y: Float) = goToNextCategory(x, y,1) private fun goToNextCategory(x: Float, y: Float, offset: Int) { + /* val sheetRect = Rect() val recyclerRect = Rect() bottom_sheet.getGlobalVisibleRect(sheetRect) @@ -594,13 +784,58 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), if (sheetRect.contains(x.toInt(), y.toInt()) || !recyclerRect.contains(x.toInt(), y.toInt())) { return + }*/ + //jumpToCategory(offset) + + + if (lockedRecycler && abs(x) > 1000f) { + val sign = sign(x).roundToInt() + val distance = recycler_layout.alpha + val speed = max(3000f / abs(x), 0.75f) + Timber.d("Flinged $distance, velo ${abs(x)}, speed $speed") + if (sign(recycler_layout.x) == sign(x)) { + flinging = true + val duration = (distance * 100 * speed).toLong() + val set = AnimatorSet() + val translationXAnimator = ValueAnimator.ofFloat(recycler_layout.x, sign * 100f) + translationXAnimator.duration = duration + translationXAnimator.addUpdateListener { animation -> + recycler_layout.x = animation.animatedValue as Float + } + + val translationAlphaAnimator = ValueAnimator.ofFloat(recycler_layout.alpha, 0f) + translationAlphaAnimator.duration = duration + translationAlphaAnimator.addUpdateListener { animation -> + recycler_layout.alpha = animation.animatedValue as Float + } + set.playTogether(translationXAnimator, translationAlphaAnimator) + set.start() + set.addListener(object : Animator.AnimatorListener { + override fun onAnimationEnd(animation: Animator?) { + recycler_layout.x = -sign * 100f + recycler_layout.alpha = 0f + scrollToHeader((if (sign <= 0) nextCategory else prevCategory) ?: -1) + resetScrollingValues() + resetRecyclerY(true, (100 * speed).toLong()) + flinging = false + } + + override fun onAnimationCancel(animation: Animator?) {} + + override fun onAnimationRepeat(animation: Animator?) {} + + override fun onAnimationStart(animation: Animator?) {} + }) + } } + } + + private fun jumpToCategory(offset: Int) { val position = (recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition() val order = when (val item = adapter.getItem(position)) { is LibraryHeaderItem -> item.category.order is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order - ?.plus(if (offset < 0) 1 else 0) else -> null } if (order != null) { @@ -608,9 +843,11 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle), while (adapter.indexOf(newOffset) == -1 && presenter.categories.any { it.order == newOffset }) { newOffset += offset } - scrollToHeader (newOffset, true) + scrollToHeader(newOffset) } } override fun popUpMenu(): PopupMenu = titlePopupMenu + + override fun recyclerIsScrolling() = switchingCategories || lockedRecycler || lockedY } \ No newline at end of file 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 fcd7a9b6c9..f21ff5ad90 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 @@ -11,6 +11,7 @@ import android.graphics.Rect import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.provider.Settings import android.view.GestureDetector import android.view.MenuItem import android.view.MotionEvent @@ -383,7 +384,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { return super.startSupportActionMode(callback) } - /* override fun onSupportActionModeFinished(mode: androidx.appcompat.view.ActionMode) { + override fun onSupportActionModeFinished(mode: androidx.appcompat.view.ActionMode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) launchUI { val scale = Settings.Global.getFloat( contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f @@ -391,10 +392,11 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { val duration = resources.getInteger(android.R.integer.config_mediumAnimTime) * scale delay(duration.toLong()) delay(100) - window?.statusBarColor = getResourceColor(android.R.attr.statusBarColor) + window?.statusBarColor = ColorUtils.setAlphaComponent(getResourceColor(android.R.attr + .colorBackground), 175) } super.onSupportActionModeFinished(mode) - }*/ + } private fun setExtensionsBadge() { val updates = preferences.extensionUpdatesCount().getOrDefault() @@ -553,6 +555,9 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { gestureDetector.onTouchEvent(ev) + val controller = router.backstack.lastOrNull()?.controller() + if (controller is OnTouchEventInterface) + controller.onTouchEvent(ev) if (ev?.action == MotionEvent.ACTION_DOWN) { if (snackBar != null && snackBar!!.isShown) { val sRect = Rect() @@ -687,9 +692,9 @@ open class MainActivity : BaseActivity(), DownloadServiceListener { && abs(diffY) <= Companion.SWIPE_THRESHOLD * 0.75f ) { if (diffX > 0) { - currentGestureDelegate?.onSwipeRight(e1.x, e1.y) + currentGestureDelegate?.onSwipeRight(velocityX, e1.y) } else { - currentGestureDelegate?.onSwipeLeft(e1.x, e1.y) + currentGestureDelegate?.onSwipeLeft(velocityX, e1.y) } result = true } @@ -739,6 +744,10 @@ interface BottomNavBarInterface { interface RootSearchInterface +interface OnTouchEventInterface { + fun onTouchEvent(event: MotionEvent?) +} + interface SpinnerTitleInterface { fun popUpMenu(): PopupMenu } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 8feefae628..855ec9c7ec 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -317,7 +317,7 @@ fun Controller.scrollViewWith(recycler: RecyclerView, swipeRefreshLayout: SwipeRefreshLayout? = null, f: ((WindowInsets) -> Unit)? = null) { var statusBarHeight = -1 - activity!!.appbar.y = 0f + activity?.appbar?.y = 0f recycler.doOnApplyWindowInsets { view, insets, _ -> val attrsArray = intArrayOf(android.R.attr.actionBarSize) val array = view.context.obtainStyledAttributes(attrsArray) @@ -335,13 +335,13 @@ fun Controller.scrollViewWith(recycler: RecyclerView, recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) - if (router.backstack.lastOrNull()?.controller() == this@scrollViewWith && + if (router?.backstack?.lastOrNull()?.controller() == this@scrollViewWith && statusBarHeight > -1 && activity!!.appbar.height > 0) { activity!!.appbar.y -= dy activity!!.appbar.y = clamp( activity!!.appbar.y, - -activity!!.appbar.height.toFloat(),// + statusBarHeight, + -activity!!.appbar.height.toFloat(), 0f ) } @@ -350,7 +350,7 @@ fun Controller.scrollViewWith(recycler: RecyclerView, override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (newState == RecyclerView.SCROLL_STATE_IDLE) { - if (router.backstack.lastOrNull()?.controller() == this@scrollViewWith && + if (router?.backstack?.lastOrNull()?.controller() == this@scrollViewWith && statusBarHeight > -1 && activity!!.appbar.height > 0) { val halfWay = abs((-activity!!.appbar.height.toFloat()) / 2) From 8134d4c2fa2f4f7e4e7466fb2a01109f1224705c Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 9 Mar 2020 21:11:09 -0700 Subject: [PATCH 02/20] Showing name of chapters that are oneshots or just weird --- .../tachiyomi/ui/manga/MangaHeaderHolder.kt | 20 ++++++++++--------- app/src/main/res/values/strings.xml | 4 +++- 2 files changed, 14 insertions(+), 10 deletions(-) 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 02c1f46768..ab714f43ab 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 @@ -157,15 +157,17 @@ class MangaHeaderHolder( visibleIf(nextChapter != null && !item.isLocked) if (nextChapter != null) { val number = adapter.decimalFormat.format(nextChapter.chapter_number.toDouble()) - text = resources.getString( - when { - nextChapter.last_page_read > 0 && nextChapter.chapter_number <= 0 -> - R.string.continue_reading - nextChapter.chapter_number <= 0 -> R.string.start_reading - nextChapter.last_page_read > 0 -> R.string.continue_reading_chapter - else -> R.string.start_reader_chapter - }, number + text = if (nextChapter.chapter_number > 0) resources.getString( + if (nextChapter.last_page_read > 0) R.string.continue_reading_chapter + else R.string.start_reading_chapter, number ) + else { + val name = nextChapter.name + resources.getString( + if (nextChapter.last_page_read > 0) R.string.continue_reading_x + else R.string.start_reading_x, name + ) + } } } @@ -173,7 +175,7 @@ class MangaHeaderHolder( chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count) top_view.updateLayoutParams { - height = adapter.coverListener.topCoverHeight() ?: 0 + height = adapter.coverListener.topCoverHeight() } manga_status.text = (itemView.context.getString( when (manga.status) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 510671c57a..de516fde15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -515,7 +515,9 @@ Start reading - Start reading Chapter %1$s + Start reading %1$s + Continue reading %1$s + Start reading Chapter %1$s Continue reading Chapter %1$s Continue reading Chapters From 29134f6bb0765f9793405a73126880176d26b54d Mon Sep 17 00:00:00 2001 From: Jay Date: Mon, 9 Mar 2020 23:34:00 -0700 Subject: [PATCH 03/20] Added tracking sheet to manga details Using the compact card view dev tachi is for now, maybe forever Also finally fixed the anilist bug Co-Authored-By: arkon --- .../data/track/anilist/AnilistModels.kt | 36 ++-- .../ui/manga/MangaDetailsController.kt | 35 ++++ .../ui/manga/MangaDetailsPresenter.kt | 129 +++++++++++- .../tachiyomi/ui/manga/MangaHeaderHolder.kt | 17 +- .../tachiyomi/ui/manga/TrackingBottomSheet.kt | 185 ++++++++++++++++++ .../ui/manga/chapter/ChaptersAdapter.kt | 1 + .../ui/manga/track/SetTrackChaptersDialog.kt | 8 +- .../ui/manga/track/SetTrackScoreDialog.kt | 8 +- .../ui/manga/track/SetTrackStatusDialog.kt | 9 +- .../tachiyomi/ui/manga/track/TrackAdapter.kt | 11 +- .../ui/manga/track/TrackController.kt | 2 +- .../tachiyomi/ui/manga/track/TrackHolder.kt | 20 +- .../ui/manga/track/TrackSearchDialog.kt | 35 +++- .../tachiyomi/util/view/ViewExtensions.kt | 2 +- app/src/main/res/layout/track_item.xml | 170 ++++++++-------- .../main/res/layout/tracking_bottom_sheet.xml | 14 ++ app/src/main/res/values/styles.xml | 2 +- 17 files changed, 526 insertions(+), 158 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/TrackingBottomSheet.kt create mode 100644 app/src/main/res/layout/tracking_bottom_sheet.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt index 6877b6efb6..5eba6f373c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/anilist/AnilistModels.kt @@ -1,18 +1,13 @@ package eu.kanade.tachiyomi.data.track.anilist -import android.app.DownloadManager -import android.content.Context -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.model.TrackSearch -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.text.SimpleDateFormat -import java.util.* +import java.util.Locale data class ALManga( val media_id: Int, @@ -45,12 +40,11 @@ data class ALManga( } data class ALUserManga( - val library_id: Long, - val list_status: String, - val score_raw: Int, - val chapters_read: Int, - val manga: ALManga, - val context: Context = Injekt.get().context + val library_id: Long, + val list_status: String, + val score_raw: Int, + val chapters_read: Int, + val manga: ALManga ) { fun toTrack() = Track.create(TrackManager.ANILIST).apply { @@ -62,16 +56,14 @@ data class ALUserManga( total_chapters = manga.total_chapters } - fun toTrackStatus() = with(context) { - when (list_status) { - getString(R.string.reading) -> Anilist.READING - getString(R.string.completed) -> Anilist.COMPLETED - getString(R.string.paused) -> Anilist.PAUSED - getString(R.string.dropped) -> Anilist.DROPPED - getString(R.string.plan_to_read) -> Anilist.PLANNING - getString(R.string.repeating)-> Anilist.REPEATING - else -> throw NotImplementedError("Unknown status") - } + fun toTrackStatus() = when (list_status) { + "CURRENT" -> Anilist.READING + "COMPLETED" -> Anilist.COMPLETED + "PAUSED" -> Anilist.PAUSED + "DROPPED" -> Anilist.DROPPED + "PLANNING" -> Anilist.PLANNING + "REPEATING" -> Anilist.REPEATING + else -> throw NotImplementedError("Unknown status") } } 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 5c384b9bfd..42e293b689 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 @@ -61,6 +61,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.notification.NotificationReceiver +import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource @@ -78,6 +79,7 @@ import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog import eu.kanade.tachiyomi.ui.manga.info.EditMangaDialog +import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.webview.WebViewActivity @@ -143,6 +145,7 @@ class MangaDetailsController : BaseController, private var snack: Snackbar? = null val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false) var coverDrawable:Drawable? = null + var trackingBottomSheet: TrackingBottomSheet? = null /** * Adapter containing a list of chapters. */ @@ -444,6 +447,8 @@ class MangaDetailsController : BaseController, override fun onDestroyView(view: View) { snack?.dismiss() presenter.onDestroy() + adapter = null + trackingBottomSheet = null super.onDestroyView(view) } @@ -869,6 +874,36 @@ class MangaDetailsController : BaseController, return super.handleBack() } + override fun showTrackingSheet() { + trackingBottomSheet = TrackingBottomSheet(this) + trackingBottomSheet?.show() + } + + fun refreshTracking(trackings: List) { + trackingBottomSheet?.onNextTrackings(trackings) + } + + fun onTrackSearchResults(results: List) { + trackingBottomSheet?.onSearchResults(results) + } + + fun refreshTracker() { + (recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder) + ?.updateTracking() + } + + fun trackRefreshDone() { + trackingBottomSheet?.onRefreshDone() + } + + fun trackRefreshError(error: Exception) { + trackingBottomSheet?.onRefreshError(error) + } + + fun trackSearchError(error: Exception) { + trackingBottomSheet?.onSearchResultsError(error) + } + override fun zoomImageFromThumb(thumbView: View) { // If there's an animation in progress, cancel it immediately and proceed with this one. currentAnimator?.cancel() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index 1e4c66c524..ae4975432f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -20,11 +20,13 @@ import eu.kanade.tachiyomi.data.library.LibraryServiceListener import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem +import eu.kanade.tachiyomi.ui.manga.track.TrackItem import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.storage.DiskUtil @@ -62,6 +64,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController, private val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } var tracks = emptyList() + var trackList: List = emptyList() var chapters:List = emptyList() private set @@ -73,6 +76,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController, headerItem.isLocked = isLockedFromSearch downloadManager.addListener(this) LibraryUpdateService.setListener(this) + tracks = db.getTracks(manga).executeAsBlocking() if (!manga.initialized) { isLoading = true controller.setRefresh(true) @@ -81,9 +85,9 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController, } else { updateChapters() - tracks = db.getTracks(manga).executeAsBlocking() controller.updateChapters(this.chapters) } + fetchTrackings() } fun onDestroy() { @@ -161,7 +165,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController, } /** * Sets the active display mode. - * @param mode the mode to set. + * @param hide set title to hidden */ fun hideTitle(hide: Boolean) { manga.displayMode = if (hide) Manga.DISPLAY_NUMBER else Manga.DISPLAY_NAME @@ -658,7 +662,124 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController, return false } - fun isTracked(): Boolean { - return loggedServices.any { service -> tracks.any { it.sync_id == service.id } } + fun isTracked(): Boolean = loggedServices.any { service -> tracks.any { it.sync_id == service.id } } + + fun hasTrackers(): Boolean = loggedServices.isNotEmpty() + + + // Tracking + + private fun fetchTrackings() { + launch { + trackList = loggedServices.map { service -> + TrackItem(tracks.find { it.sync_id == service.id }, service) + } + } + } + + private suspend fun refreshTracking() { + tracks = withContext(Dispatchers.IO) { db.getTracks(manga).executeAsBlocking() } + trackList = loggedServices.map { service -> + TrackItem(tracks.find { it.sync_id == service.id }, service) + } + withContext(Dispatchers.Main) { controller.refreshTracking(trackList) } + } + + fun refreshTrackers() { + launch { + val list = trackList.filter { it.track != null }.map { item -> + withContext(Dispatchers.IO) { + val trackItem = try { + item.service.refresh(item.track!!).toBlocking().single() + } catch (e: Exception) { + trackError(e) + null + } + if (trackItem != null) { + db.insertTrack(trackItem).executeAsBlocking() + trackItem + } + else + item.track + } + } + refreshTracking() + } + } + + fun trackSearch(query: String, service: TrackService) { + launch(Dispatchers.IO) { + val results = try {service.search(query).toBlocking().single() } + catch (e: Exception) { + withContext(Dispatchers.Main) { controller.trackSearchError(e) } + null } + if (!results.isNullOrEmpty()) { + withContext(Dispatchers.Main) { controller.onTrackSearchResults(results) } + } + } + } + + fun registerTracking(item: Track?, service: TrackService) { + if (item != null) { + item.manga_id = manga.id!! + + launch { + val binding = try { service.bind(item).toBlocking().single() } + catch (e: Exception) { + trackError(e) + null + } + withContext(Dispatchers.IO) { + if (binding != null) db.insertTrack(binding).executeAsBlocking() } + refreshTracking() + } + } else { + launch { + withContext(Dispatchers.IO) { db.deleteTrackForManga(manga, service) + .executeAsBlocking() } + refreshTracking() + } + } + } + + private fun updateRemote(track: Track, service: TrackService) { + launch { + val binding = try { service.update(track).toBlocking().single() } + catch (e: Exception) { + trackError(e) + null + } + if (binding != null) { + withContext(Dispatchers.IO) { db.insertTrack(binding).executeAsBlocking() } + refreshTracking() + } + else trackRefreshDone() + } + } + + private suspend fun trackRefreshDone() { + async(Dispatchers.Main) { controller.trackRefreshDone() } + } + + private suspend fun trackError(error: Exception) { + async(Dispatchers.Main) { controller.trackRefreshError(error) } + } + + fun setStatus(item: TrackItem, index: Int) { + val track = item.track!! + track.status = item.service.getStatusList()[index] + updateRemote(track, item.service) + } + + fun setScore(item: TrackItem, index: Int) { + val track = item.track!! + track.score = item.service.indexToScore(index) + updateRemote(track, item.service) + } + + fun setLastChapterRead(item: TrackItem, chapterNumber: Int) { + val track = item.track!! + track.last_chapter_read = chapterNumber + updateRemote(track, item.service) } } \ 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 ab714f43ab..7fa6f69282 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 @@ -54,7 +54,7 @@ class MangaHeaderHolder( filters_text.setOnClickListener { adapter.coverListener?.showChapterFilter() } chapters_title.setOnClickListener { adapter.coverListener?.showChapterFilter() } webview_button.setOnClickListener { adapter.coverListener?.openInWebView() } - share_button.setOnClickListener { adapter.coverListener?.prepareToShareManga() } + share_button.setOnClickListener { adapter.coverListener?.prepareToShareManga() } favorite_button.setOnClickListener { adapter.coverListener?.favoriteManga(false) } @@ -71,6 +71,7 @@ class MangaHeaderHolder( true } manga_cover.setOnClickListener { adapter.coverListener?.zoomImageFromThumb(cover_card) } + track_button.setOnClickListener { adapter.coverListener?.showTrackingSheet() } if (startExpanded) expandDesc() } @@ -144,6 +145,7 @@ class MangaHeaderHolder( val tracked = presenter.isTracked() && !item.isLocked with(track_button) { + visibleIf(presenter.hasTrackers()) text = itemView.context.getString(if (tracked) R.string.action_filter_tracked else R.string.tracking) @@ -232,6 +234,19 @@ class MangaHeaderHolder( true_backdrop.setBackgroundColor(color) } + fun updateTracking() { + val presenter = adapter.coverListener?.mangaPresenter() ?: return + val tracked = presenter.isTracked() + with(track_button) { + text = itemView.context.getString(if (tracked) R.string.action_filter_tracked + else R.string.tracking) + + icon = ContextCompat.getDrawable(itemView.context, if (tracked) R.drawable + .ic_check_white_24dp else R.drawable.ic_sync_black_24dp) + checked(tracked) + } + } + override fun onLongClick(view: View?): Boolean { super.onLongClick(view) return false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/TrackingBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/TrackingBottomSheet.kt new file mode 100644 index 0000000000..63c7b35608 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/TrackingBottomSheet.kt @@ -0,0 +1,185 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.ui.manga.track.SetTrackChaptersDialog +import eu.kanade.tachiyomi.ui.manga.track.SetTrackScoreDialog +import eu.kanade.tachiyomi.ui.manga.track.SetTrackStatusDialog +import eu.kanade.tachiyomi.ui.manga.track.TrackAdapter +import eu.kanade.tachiyomi.ui.manga.track.TrackHolder +import eu.kanade.tachiyomi.ui.manga.track.TrackItem +import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog +import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener +import eu.kanade.tachiyomi.util.view.setEdgeToEdge +import kotlinx.android.synthetic.main.tracking_bottom_sheet.* +import timber.log.Timber + +class TrackingBottomSheet(private val controller: MangaDetailsController) : BottomSheetDialog + (controller.activity!!, R.style.BottomSheetDialogTheme), + TrackAdapter.OnClickListener, + SetTrackStatusDialog.Listener, + SetTrackChaptersDialog.Listener, + SetTrackScoreDialog.Listener { + + val activity = controller.activity!! + + private var sheetBehavior: BottomSheetBehavior<*> + + val presenter = controller.presenter + + private var adapter: TrackAdapter? = null + + init { + // Use activity theme for this layout + val view = activity.layoutInflater.inflate(R.layout.tracking_bottom_sheet, null) + setContentView(view) + + sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup) + setEdgeToEdge(activity, display_bottom_sheet, view, false) + val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + activity.window.decorView.rootWindowInsets.systemWindowInsetBottom + } else 0 + sheetBehavior.peekHeight = 380.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 + } + + /** + * Called when the sheet is created. It initializes the listeners and values of the preferences. + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + adapter = TrackAdapter(this) + track_recycler.layoutManager = LinearLayoutManager(context) + track_recycler.adapter = adapter + track_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + + adapter?.items = presenter.trackList + } + + fun onNextTrackings(trackings: List) { + onRefreshDone() + adapter?.items = trackings + controller.refreshTracker() + } + + fun onSearchResults(results: List) { + getSearchDialog()?.onSearchResults(results) + } + + fun onSearchResultsError(error: Throwable) { + Timber.e(error) + getSearchDialog()?.onSearchResultsError() + } + + private fun getSearchDialog(): TrackSearchDialog? { + return controller.router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog + } + + fun onRefreshDone() { + for (i in adapter!!.items.indices) { + (track_recycler.findViewHolderForAdapterPosition(i) as? TrackHolder)?.setProgress(false) + } + } + + fun onRefreshError(error: Throwable) { + for (i in adapter!!.items.indices) { + (track_recycler.findViewHolderForAdapterPosition(i) as? TrackHolder)?.setProgress(false) + } + activity.toast(error.message) + } + + override fun onLogoClick(position: Int) { + val track = adapter?.getItem(position)?.track ?: return + + if (track.tracking_url.isBlank()) { + activity.toast(R.string.url_not_set) + } else { + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url))) + } + } + + override fun onSetClick(position: Int) { + val item = adapter?.getItem(position) ?: return + TrackSearchDialog(this, item.service, item.track != null).showDialog( + controller.router, TAG_SEARCH_CONTROLLER) + } + + override fun onStatusClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackStatusDialog(this, item).showDialog(controller.router) + } + + override fun onChaptersClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackChaptersDialog(this, item).showDialog(controller.router) + } + + override fun onScoreClick(position: Int) { + val item = adapter?.getItem(position) ?: return + if (item.track == null) return + + SetTrackScoreDialog(this, item).showDialog(controller.router) + } + + override fun setStatus(item: TrackItem, selection: Int) { + presenter.setStatus(item, selection) + refreshItem(item) + } + + private fun refreshItem(item: TrackItem) { + refreshTrack(item.service) + } + + fun refreshTrack(item: TrackService?) { + val index = adapter?.indexOf(item) ?: -1 + if (index > -1 ){ + (track_recycler.findViewHolderForAdapterPosition(index) as? TrackHolder) + ?.setProgress(true) + } + } + + override fun setScore(item: TrackItem, score: Int) { + presenter.setScore(item, score) + refreshItem(item) + } + + override fun setChaptersRead(item: TrackItem, chaptersRead: Int) { + presenter.setLastChapterRead(item, chaptersRead) + refreshItem(item) + } + + private companion object { + const val TAG_SEARCH_CONTROLLER = "track_search_controller" + } +} 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 ff7375c5fa..906391f69e 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 @@ -71,5 +71,6 @@ class ChaptersAdapter( fun favoriteManga(longPress: Boolean) fun copyToClipboard(content: String, label: Int) fun zoomImageFromThumb(thumbView: View) + fun showTrackingSheet() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt index dd3f65d98f..5f59acea52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt @@ -6,7 +6,6 @@ import android.widget.NumberPicker import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.customview.customView import com.afollestad.materialdialogs.customview.getCustomView -import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager @@ -15,14 +14,15 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SetTrackChaptersDialog : DialogController - where T : Controller, T : SetTrackChaptersDialog.Listener { + where T : SetTrackChaptersDialog.Listener { private val item: TrackItem + private lateinit var listener: Listener constructor(target: T, item: TrackItem) : super(Bundle().apply { putSerializable(KEY_ITEM_TRACK, item.track) }) { - targetController = target + listener = target this.item = item } @@ -45,7 +45,7 @@ class SetTrackChaptersDialog : DialogController // Remove focus to update selected number val np: NumberPicker = view.findViewById(R.id.chapters_picker) np.clearFocus() - (targetController as? Listener)?.setChaptersRead(item, np.value) + listener.setChaptersRead(item, np.value) } val view = dialog.getCustomView() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt index 6aac10037e..b85280289f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt @@ -15,14 +15,15 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SetTrackScoreDialog : DialogController - where T : Controller, T : SetTrackScoreDialog.Listener { + where T : SetTrackScoreDialog.Listener { private val item: TrackItem + private lateinit var listener: Listener constructor(target: T, item: TrackItem) : super(Bundle().apply { putSerializable(KEY_ITEM_TRACK, item.track) }) { - targetController = target + listener = target this.item = item } @@ -46,8 +47,7 @@ class SetTrackScoreDialog : DialogController val np: NumberPicker = view.findViewById(R.id.score_picker) np.clearFocus() - (targetController as? Listener)?.setScore(item, np.value) - + listener.setScore(item, np.value) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt index 42ccd06a9f..7047b8f39c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt @@ -4,7 +4,6 @@ import android.app.Dialog import android.os.Bundle import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.list.listItemsSingleChoice -import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.track.TrackManager @@ -13,14 +12,16 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get class SetTrackStatusDialog : DialogController - where T : Controller, T : SetTrackStatusDialog.Listener { + where T : SetTrackStatusDialog.Listener { private val item: TrackItem + private lateinit var listener: Listener constructor(target: T, item: TrackItem) : super(Bundle().apply { putSerializable(KEY_ITEM_TRACK, item.track) }) { - targetController = target + listener = target + // targetController = target this.item = item } @@ -43,7 +44,7 @@ class SetTrackStatusDialog : DialogController .listItemsSingleChoice(items = statusString, initialSelection = selectedIndex, waitForPositiveButton = false) { dialog, position, _ -> - (targetController as? Listener)?.setStatus(item, position) + listener.setStatus(item, position) dialog.dismiss() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt index 4f4daf86c5..9d8599e516 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt @@ -1,11 +1,12 @@ package eu.kanade.tachiyomi.ui.manga.track -import androidx.recyclerview.widget.RecyclerView import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.util.view.inflate -class TrackAdapter(controller: TrackController) : RecyclerView.Adapter() { +class TrackAdapter(controller: OnClickListener) : RecyclerView.Adapter() { var items = emptyList() set(value) { @@ -34,9 +35,13 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter(), } } - override fun onTitleClick(position: Int) { + override fun onSetClick(position: Int) { val item = adapter?.getItem(position) ?: return TrackSearchDialog(this, item.service, item.track != null).showDialog(router, TAG_SEARCH_CONTROLLER) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index c8c9da6887..d56d97eea6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.manga.track import android.annotation.SuppressLint import android.view.View -import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder +import eu.kanade.tachiyomi.util.view.visibleIf import kotlinx.android.synthetic.main.track_item.* class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { @@ -11,32 +11,28 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { init { val listener = adapter.rowClickListener logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) } - title_container.setOnClickListener { listener.onTitleClick(adapterPosition) } + track_set.setOnClickListener { listener.onSetClick(adapterPosition) } status_container.setOnClickListener { listener.onStatusClick(adapterPosition) } chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) } score_container.setOnClickListener { listener.onScoreClick(adapterPosition) } } @SuppressLint("SetTextI18n") - @Suppress("DEPRECATION") fun bind(item: TrackItem) { val track = item.track track_logo.setImageResource(item.service.getLogo()) logo_container.setBackgroundColor(item.service.getLogoColor()) + track_group.visibleIf(track != null) if (track != null) { - track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary) - track_title.isAllCaps = false - track_title.text = track.title track_chapters.text = "${track.last_chapter_read}/" + if (track.total_chapters > 0) track.total_chapters else "-" track_status.text = item.service.getStatus(track.status) track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track) - } else { - track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button) - track_title.setText(R.string.action_edit) - track_chapters.text = "" - track_score.text = "" - track_status.text = "" } } + + fun setProgress(enabled: Boolean) { + progress.visibleIf(enabled) + track_logo.visibleIf(!enabled) + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt index e6f6337f10..d5c5ab3c41 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt @@ -15,11 +15,10 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.ui.base.controller.DialogController -import kotlinx.android.synthetic.main.track_controller.* +import eu.kanade.tachiyomi.ui.manga.MangaDetailsPresenter +import eu.kanade.tachiyomi.ui.manga.TrackingBottomSheet import eu.kanade.tachiyomi.util.lang.plusAssign -import kotlinx.android.synthetic.main.track_search_dialog.view.progress -import kotlinx.android.synthetic.main.track_search_dialog.view.track_search -import kotlinx.android.synthetic.main.track_search_dialog.view.track_search_list +import kotlinx.android.synthetic.main.track_search_dialog.view.* import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.subscriptions.CompositeSubscription @@ -41,10 +40,14 @@ class TrackSearchDialog : DialogController { private var searchTextSubscription: Subscription? = null - private val trackController - get() = targetController as TrackController + private lateinit var bottomSheet: TrackingBottomSheet + //private val trackController + // get() = targetController as TrackController + + private var wasPreviouslyTracked:Boolean = false + private lateinit var presenter:MangaDetailsPresenter constructor(target: TrackController, service: TrackService, wasTracked:Boolean) : super(Bundle() .apply { @@ -55,6 +58,16 @@ class TrackSearchDialog : DialogController { this.service = service } + constructor(target: TrackingBottomSheet, service: TrackService, wasTracked:Boolean) : super(Bundle() + .apply { + putInt(KEY_SERVICE, service.id) + }) { + wasPreviouslyTracked = wasTracked + bottomSheet = target + presenter = target.presenter + this.service = service + } + @Suppress("unused") constructor(bundle: Bundle) : super(bundle) { service = Injekt.get().getService(bundle.getInt(KEY_SERVICE))!! @@ -97,7 +110,7 @@ class TrackSearchDialog : DialogController { // Do an initial search based on the manga's title if (savedState == null) { - val title = trackController.presenter.manga.originalTitle() + val title = presenter.manga.originalTitle() view.track_search.append(title) search(title) } @@ -129,7 +142,7 @@ class TrackSearchDialog : DialogController { val view = dialogView ?: return view.progress.visibility = View.VISIBLE view.track_search_list.visibility = View.INVISIBLE - trackController.presenter.search(query, service) + presenter.trackSearch(query, service) } fun onSearchResults(results: List) { @@ -153,8 +166,10 @@ class TrackSearchDialog : DialogController { } private fun onPositiveButtonClick() { - trackController.swipe_refresh.isRefreshing = true - trackController.presenter.registerTracking(selectedItem, service) + // trackController.swipe_refresh.isRefreshing = true + bottomSheet.refreshTrack(service) + presenter.registerTracking(selectedItem, + service) } private companion object { diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 855ec9c7ec..1fb821f6a3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -169,7 +169,7 @@ inline val View.marginLeft: Int object RecyclerWindowInsetsListener : View.OnApplyWindowInsetsListener { override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets { - v.setPadding(0,0,0,insets.systemWindowInsetBottom) + v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom) //v.updatePaddingRelative(bottom = v.paddingBottom + insets.systemWindowInsetBottom) return insets } diff --git a/app/src/main/res/layout/track_item.xml b/app/src/main/res/layout/track_item.xml index ba771d41c4..f0a71d0f1e 100644 --- a/app/src/main/res/layout/track_item.xml +++ b/app/src/main/res/layout/track_item.xml @@ -1,22 +1,29 @@ - + android:layout_height="wrap_content" + android:background="?android:attr/colorBackground" + android:minHeight="?attr/actionBarSize"> - - - - - + android:layout_gravity="center" + android:padding="4dp" + android:visibility="gone"/> - + - - - + + android:focusable="true" + android:orientation="vertical" + app:layout_constraintStart_toEndOf="@id/logo_container" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintEnd_toStartOf="@id/chapters_container" + app:layout_constraintBottom_toBottomOf="parent" + android:paddingStart="16dp" + android:paddingTop="16dp" + android:paddingEnd="8dp" + android:paddingBottom="16dp"> + android:layout_marginTop="8dp" + tools:text="Currently Reading" /> - - + android:focusable="true" + android:orientation="vertical" + android:paddingStart="8dp" + android:paddingTop="16dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/status_container" + app:layout_constraintEnd_toStartOf="@id/score_container" + app:layout_constraintBottom_toBottomOf="parent" + android:paddingEnd="8dp" + android:paddingBottom="16dp"> - - + android:focusable="true" + android:orientation="vertical" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toEndOf="@id/chapters_container" + app:layout_constraintEnd_toStartOf="@id/track_set" + android:paddingStart="8dp" + android:paddingTop="16dp" + android:paddingEnd="16dp" + android:paddingBottom="16dp"> + + - + \ No newline at end of file diff --git a/app/src/main/res/layout/tracking_bottom_sheet.xml b/app/src/main/res/layout/tracking_bottom_sheet.xml new file mode 100644 index 0000000000..2bb015b59c --- /dev/null +++ b/app/src/main/res/layout/tracking_bottom_sheet.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 3cd736afed..e943e0b88d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -149,7 +149,7 @@ eu.kanade.tachiyomi.widget.FABMoveBehaviour - From e68a1ae48d30f5823b2b9d303b43dbfe0e653a24 Mon Sep 17 00:00:00 2001 From: Jay Date: Wed, 11 Mar 2020 00:28:47 -0700 Subject: [PATCH 11/20] Extensions now in a bottom sheet on the browse tab Boy I love bottom sheets guys --- .../ui/catalogue/CatalogueController.kt | 113 +++++++-- .../ui/catalogue/CataloguePresenter.kt | 4 +- .../ui/extension/ExtensionAdapter.kt | 12 +- .../ui/extension/ExtensionBottomPresenter.kt | 153 ++++++++++++ .../ui/extension/ExtensionBottomSheet.kt | 224 ++++++++++++++++++ .../ui/extension/ExtensionPresenter.kt | 2 +- .../ui/extension/ExtensionTrustDialog.kt | 10 +- .../ui/setting/SettingsMainController.kt | 8 - .../tachiyomi/util/view/ViewExtensions.kt | 10 +- .../widget/preference/ExtensionPreference.kt | 2 +- .../res/color/btn_bg_primary_selector.xml | 6 + .../main/res/drawable/ic_sync_black_24dp.xml | 1 + app/src/main/res/layout/auto_ext_checkbox.xml | 9 + .../res/layout/catalogue_main_controller.xml | 47 +++- .../catalouge_more_extensions_card_item.xml | 43 ++++ .../main/res/layout/extension_controller.xml | 10 +- .../res/layout/extensions_bottom_sheet.xml | 64 +++++ app/src/main/res/menu/extension_main.xml | 12 +- app/src/main/res/values/strings.xml | 8 +- 19 files changed, 675 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt create mode 100644 app/src/main/res/color/btn_bg_primary_selector.xml create mode 100644 app/src/main/res/layout/auto_ext_checkbox.xml create mode 100644 app/src/main/res/layout/catalouge_more_extensions_card_item.xml create mode 100644 app/src/main/res/layout/extensions_bottom_sheet.xml 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 787c43a758..40ba5cd54a 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 @@ -13,7 +13,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler -import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents +import com.google.android.material.bottomsheet.BottomSheetBehavior import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R @@ -26,17 +26,20 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.RootSearchInterface import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController -import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener 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.widget.preference.SourceLoginDialog import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.catalogue_main_controller.* +import kotlinx.android.synthetic.main.extensions_bottom_sheet.* import kotlinx.android.synthetic.main.main_activity.* import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import kotlin.math.max /** * This controller shows and manages the different catalogues enabled by the user. @@ -62,6 +65,13 @@ class CatalogueController : NucleusController(), */ private var adapter: CatalogueAdapter? = null + var extQuery = "" + private set + + var headerHeight = 0 + + var customTitle = "" + /** * Called when controller is initialized. */ @@ -76,7 +86,9 @@ class CatalogueController : NucleusController(), * @return title. */ override fun getTitle(): String? { - return applicationContext?.getString(R.string.label_catalogues) + return if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) + applicationContext?.getString(R.string.label_extensions) + else applicationContext?.getString(R.string.label_catalogues) } /** @@ -114,11 +126,41 @@ class CatalogueController : NucleusController(), recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context) recycler.adapter = adapter recycler.addItemDecoration(SourceDividerItemDecoration(view.context)) - recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) - - scrollViewWith(recycler) + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = view.context.obtainStyledAttributes(attrsArray) + val appBarHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + scrollViewWith(recycler) { + headerHeight = it.systemWindowInsetTop + appBarHeight + } requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301) + ext_bottom_sheet.onCreate(this) + + ext_bottom_sheet.sheetBehavior?.addBottomSheetCallback(object : BottomSheetBehavior + .BottomSheetCallback() { + override fun onSlide(bottomSheet: View, progress: Float) { + shadow2.alpha = (1 - max(0f, progress)) * 0.25f + sheet_layout.alpha = 1 - progress + activity?.appbar?.y = max(activity!!.appbar.y, -headerHeight * (1 - progress)) + } + + override fun onStateChanged(p0: View, state: Int) { + 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 + + retainViewMode = if (state == BottomSheetBehavior.STATE_EXPANDED) + RetainViewMode.RETAIN_DETACH else RetainViewMode.RELEASE_DETACH + activity?.invalidateOptionsMenu() + setTitle() + sheet_layout.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED + sheet_layout.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED + } + }) + } override fun onDestroyView(view: View) { @@ -129,6 +171,7 @@ class CatalogueController : NucleusController(), override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { super.onChangeStarted(handler, type) if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) { + ext_bottom_sheet.updateExtTitle() presenter.updateSources() } } @@ -192,20 +235,41 @@ class CatalogueController : NucleusController(), * @param inflater used to load the menu xml. */ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - // Inflate menu - inflater.inflate(R.menu.catalogue_main, menu) + if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) { + // Inflate menu + inflater.inflate(R.menu.extension_main, menu) - // Initialize search option. - val searchItem = menu.findItem(R.id.action_search) - val searchView = searchItem.actionView as SearchView + // Initialize search option. + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView - // Change hint to show global search. - searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint) + // Change hint to show global search. + searchView.queryHint = applicationContext?.getString(R.string.search_extensions) - // Create query listener which opens the global search view. - searchView.queryTextChangeEvents() - .filter { it.isSubmitted } - .subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) } + // Create query listener which opens the global search view. + setOnQueryTextChangeListener(searchView) { + extQuery = it ?: "" + ext_bottom_sheet.drawExtensions() + true + } + } + else { + // Inflate menu + inflater.inflate(R.menu.catalogue_main, menu) + + // Initialize search option. + val searchItem = menu.findItem(R.id.action_search) + val searchView = searchItem.actionView as SearchView + + // Change hint to show global search. + searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint) + + // Create query listener which opens the global search view. + setOnQueryTextChangeListener(searchView, true) { + if (!it.isNullOrBlank()) performGlobalSearch(it) + true + } + } } private fun performGlobalSearch(query: String){ @@ -222,9 +286,18 @@ class CatalogueController : NucleusController(), when (item.itemId) { // Initialize option to open catalogue settings. R.id.action_filter -> { - router.pushController((RouterTransaction.with(SettingsSourcesController())) - .popChangeHandler(SettingsSourcesFadeChangeHandler()) - .pushChangeHandler(FadeChangeHandler())) + val controller = + if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) + SettingsExtensionsController() + else SettingsSourcesController() + router.pushController( + (RouterTransaction.with(controller)).popChangeHandler( + SettingsSourcesFadeChangeHandler() + ).pushChangeHandler(FadeChangeHandler()) + ) + } + R.id.action_dismiss -> { + ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED } else -> return super.onOptionsItemSelected(item) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index 096c5b19e7..ae5611e70a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -12,7 +12,7 @@ import rx.Subscription import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.* +import java.util.TreeMap import java.util.concurrent.TimeUnit /** @@ -101,4 +101,4 @@ class CataloguePresenter( .sortedBy { "(${it.lang}) ${it.name}" } + sourceManager.get(LocalSource.ID) as LocalSource } -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt index f8b1f56715..d40c17c7e5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt @@ -3,17 +3,19 @@ package eu.kanade.tachiyomi.ui.extension import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.items.IFlexible import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.extension.ExtensionAdapter.OnButtonClickListener import eu.kanade.tachiyomi.util.system.getResourceColor /** * Adapter that holds the catalogue cards. * - * @param controller instance of [ExtensionController]. + * @param listener instance of [OnButtonClickListener]. */ -class ExtensionAdapter(val controller: ExtensionController) : - FlexibleAdapter>(null, controller, true) { +class ExtensionAdapter(val listener: OnButtonClickListener) : + FlexibleAdapter>(null, listener, true) { - val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card) + val cardBackground = (listener as ExtensionBottomSheet).context.getResourceColor(R.attr + .background_card) init { setDisplayHeadersAtStartUp(true) @@ -22,7 +24,7 @@ class ExtensionAdapter(val controller: ExtensionController) : /** * Listener for browse item clicks. */ - val buttonClickListener: ExtensionAdapter.OnButtonClickListener = controller + val buttonClickListener: ExtensionAdapter.OnButtonClickListener = listener interface OnButtonClickListener { fun onButtonClick(position: Int) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt new file mode 100644 index 0000000000..2c1793ae6a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt @@ -0,0 +1,153 @@ +package eu.kanade.tachiyomi.ui.extension + +import android.app.Application +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.extension.ExtensionManager +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.extension.model.InstallStep +import eu.kanade.tachiyomi.util.system.LocaleHelper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import rx.Observable +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.util.concurrent.TimeUnit +import kotlin.coroutines.CoroutineContext + +/** + * Presenter of [ExtensionController]. + */ +open class ExtensionBottomPresenter( + private val bottomSheet: ExtensionBottomSheet, + private val extensionManager: ExtensionManager = Injekt.get(), + private val preferences: PreferencesHelper = Injekt.get() +) : CoroutineScope { + + override var coroutineContext: CoroutineContext = Job() + Dispatchers.Default + + private var extensions = emptyList() + + private var currentDownloads = hashMapOf() + + fun onCreate() { + extensionManager.findAvailableExtensions() + bindToExtensionsObservable() + } + + private fun bindToExtensionsObservable(): Subscription { + val installedObservable = extensionManager.getInstalledExtensionsObservable() + val untrustedObservable = extensionManager.getUntrustedExtensionsObservable() + val availableObservable = extensionManager.getAvailableExtensionsObservable() + .startWith(emptyList()) + + return Observable.combineLatest(installedObservable, untrustedObservable, availableObservable) + { installed, untrusted, available -> Triple(installed, untrusted, available) } + .debounce(100, TimeUnit.MILLISECONDS) + .map(::toItems) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + bottomSheet.setExtensions(extensions) + } + } + + @Synchronized + private fun toItems(tuple: ExtensionTuple): List { + val context = Injekt.get() + val activeLangs = preferences.enabledLanguages().getOrDefault() + + val (installed, untrusted, available) = tuple + + val items = mutableListOf() + + val installedSorted = installed.sortedWith(compareBy({ !it.hasUpdate }, { !it.isObsolete }, { it.pkgName })) + val untrustedSorted = untrusted.sortedBy { it.pkgName } + val availableSorted = available + // Filter out already installed extensions and disabled languages + .filter { avail -> installed.none { it.pkgName == avail.pkgName } + && untrusted.none { it.pkgName == avail.pkgName } + && (avail.lang in activeLangs || avail.lang == "all")} + .sortedBy { it.pkgName } + + if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) { + val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size) + items += installedSorted.map { extension -> + ExtensionItem(extension, header, currentDownloads[extension.pkgName]) + } + items += untrustedSorted.map { extension -> + ExtensionItem(extension, header) + } + } + if (availableSorted.isNotEmpty()) { + val availableGroupedByLang = availableSorted + .groupBy { LocaleHelper.getDisplayName(it.lang, context) } + .toSortedMap() + + availableGroupedByLang + .forEach { + val header = ExtensionGroupItem(it.key, it.value.size) + items += it.value.map { extension -> + ExtensionItem(extension, header, currentDownloads[extension.pkgName]) + } + } + } + + this.extensions = items + return items + } + + fun getExtensionUpdateCount():Int = preferences.extensionUpdatesCount().getOrDefault() + fun getAutoCheckPref() = preferences.automaticExtUpdates() + + @Synchronized + private fun updateInstallStep(extension: Extension, state: InstallStep): ExtensionItem? { + val extensions = extensions.toMutableList() + val position = extensions.indexOfFirst { it.extension.pkgName == extension.pkgName } + + return if (position != -1) { + val item = extensions[position].copy(installStep = state) + extensions[position] = item + + this.extensions = extensions + item + } else { + null + } + } + + fun installExtension(extension: Extension.Available) { + extensionManager.installExtension(extension).subscribeToInstallUpdate(extension) + } + + fun updateExtension(extension: Extension.Installed) { + extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension) + } + + private fun Observable.subscribeToInstallUpdate(extension: Extension) { + this.doOnNext { currentDownloads[extension.pkgName] = it } + .doOnUnsubscribe { currentDownloads.remove(extension.pkgName) } + .map { state -> updateInstallStep(extension, state) } + .subscribe { item -> + if (item != null) { + bottomSheet.downloadUpdate(item) + } + } + } + + fun uninstallExtension(pkgName: String) { + extensionManager.uninstallExtension(pkgName) + } + + fun findAvailableExtensions() { + extensionManager.findAvailableExtensions() + } + + fun trustSignature(signatureHash: String) { + extensionManager.trustSignature(signatureHash) + } + +} 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 new file mode 100644 index 0000000000..714df3bf7f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt @@ -0,0 +1,224 @@ +package eu.kanade.tachiyomi.ui.extension + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.CheckBox +import android.widget.CompoundButton +import android.widget.LinearLayout +import com.f2prateek.rx.preferences.Preference +import com.google.android.material.bottomsheet.BottomSheetBehavior +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.davidea.flexibleadapter.items.AbstractHeaderItem +import eu.davidea.flexibleadapter.items.IFlexible +import eu.davidea.viewholders.FlexibleViewHolder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.getOrDefault +import eu.kanade.tachiyomi.extension.model.Extension +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.ui.catalogue.CatalogueController +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.extensions_bottom_sheet.view.* + +class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) +: LinearLayout(context, attrs), +ExtensionAdapter.OnButtonClickListener, + FlexibleAdapter.OnItemClickListener, + FlexibleAdapter.OnItemLongClickListener, + ExtensionTrustDialog.Listener { + + var sheetBehavior: BottomSheetBehavior<*>? = null + lateinit var autoCheckItem:AutoCheckItem + + /** + * Adapter containing the list of manga from the catalogue. + */ + private var adapter: FlexibleAdapter>? = null + + val presenter = ExtensionBottomPresenter(this) + + private var extensions: List = emptyList() + + lateinit var controller: CatalogueController + + fun onCreate(controller: CatalogueController) { + // Initialize adapter, scroll listener and recycler views + autoCheckItem = AutoCheckItem(presenter.getAutoCheckPref()) + adapter = ExtensionAdapter(this) + sheetBehavior = BottomSheetBehavior.from(this) + // Create recycler and set adapter. + ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context) + ext_recycler.adapter = adapter + ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(context)) + ext_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener) + // scrollViewWith(ext_recycler, true, ext_swipe_refresh) + this.controller = controller + //ext_swipe_refresh.refreshes().subscribeUntilDestroy { + // presenter.findAvailableExtensions() + presenter.onCreate() + updateExtTitle() + + val attrsArray = intArrayOf(android.R.attr.actionBarSize) + val array = context.obtainStyledAttributes(attrsArray) + val headerHeight = array.getDimensionPixelSize(0, 0) + array.recycle() + ext_recycler.doOnApplyWindowInsets { _, windowInsets, _ -> + ext_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 + } + } + presenter.getExtensionUpdateCount() + } + + fun updateExtTitle() { + val extCount = presenter.getExtensionUpdateCount() + title_text.text = if (extCount == 0) context.getString(R.string.label_extensions) + else resources.getQuantityString(R.plurals.extensions_updates_available, extCount, + extCount) + } + + override fun onButtonClick(position: Int) { + val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return + when (extension) { + is Extension.Installed -> { + if (!extension.hasUpdate) { + openDetails(extension) + } else { + presenter.updateExtension(extension) + } + } + is Extension.Available -> { + presenter.installExtension(extension) + } + is Extension.Untrusted -> { + openTrustDialog(extension) + } + } + } + + override fun onItemClick(view: View?, position: Int): Boolean { + val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false + if (extension is Extension.Installed) { + openDetails(extension) + } else if (extension is Extension.Untrusted) { + openTrustDialog(extension) + } + + return false + } + + override fun onItemLongClick(position: Int) { + val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return + if (extension is Extension.Installed || extension is Extension.Untrusted) { + uninstallExtension(extension.pkgName) + } + } + + private fun openDetails(extension: Extension.Installed) { + val controller = ExtensionDetailsController(extension.pkgName) + this.controller.router.pushController(controller.withFadeTransaction()) + } + + private fun openTrustDialog(extension: Extension.Untrusted) { + ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName) + .showDialog(controller.router) + } + + fun setExtensions(extensions: List) { + //ext_swipe_refresh?.isRefreshing = false + this.extensions = extensions + controller.presenter.updateSources() + drawExtensions() + } + + fun drawExtensions() { + if (!controller.extQuery.isBlank()) { + adapter?.updateDataSet( + extensions.filter { + it.extension.name.contains(controller.extQuery, ignoreCase = true) + }) + } else { + adapter?.updateDataSet(extensions) + } + updateExtTitle() + setLastUsedSource() + } + + /** + * Called to set the last used catalogue at the top of the view. + */ + private fun setLastUsedSource() { + adapter?.removeAllScrollableHeaders() + adapter?.addScrollableHeader(autoCheckItem) + } + + fun downloadUpdate(item: ExtensionItem) { + adapter?.updateItem(item, item.installStep) + } + + override fun trustSignature(signatureHash: String) { + presenter.trustSignature(signatureHash) + } + + override fun uninstallExtension(pkgName: String) { + presenter.uninstallExtension(pkgName) + } +} + +class AutoCheckItem(private val autoCheck: Preference) : AbstractHeaderItem() { + + override fun getLayoutRes(): Int { + return R.layout.auto_ext_checkbox + } + + override fun createViewHolder( + view: View, adapter: FlexibleAdapter> + ): AutoCheckHolder { + return AutoCheckHolder(view, adapter, autoCheck) + } + + override fun bindViewHolder( + adapter: FlexibleAdapter>, + holder: AutoCheckHolder, + position: Int, + payloads: MutableList? + ) { + //holder.bind(autoCheck.getOrDefault()) + } + + override fun equals(other: Any?): Boolean { + return (this === other) + } + + override fun hashCode(): Int { + return -1 + } + + class AutoCheckHolder(val view: View, private val adapter: FlexibleAdapter>, + autoCheck: Preference) : + FlexibleViewHolder(view, adapter, true) { + private val autoCheckbox: CheckBox = view.findViewById(R.id.auto_checkbox) + + init { + autoCheckbox.bindToPreference(autoCheck) + } + + /** + * Binds a checkbox or switch view with a boolean preference. + */ + private fun CompoundButton.bindToPreference(pref: Preference) { + isChecked = pref.getOrDefault() + setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionPresenter.kt index 04d2a8de3a..5830d35569 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionPresenter.kt @@ -17,7 +17,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit -private typealias ExtensionTuple +typealias ExtensionTuple = Triple, List, List> /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt index 577b27fd0a..3b16e4361b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionTrustDialog.kt @@ -3,18 +3,18 @@ package eu.kanade.tachiyomi.ui.extension import android.app.Dialog import android.os.Bundle import com.afollestad.materialdialogs.MaterialDialog -import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.controller.DialogController class ExtensionTrustDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T: ExtensionTrustDialog.Listener { + where T: ExtensionTrustDialog.Listener { + lateinit var listener: Listener constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply { putString(SIGNATURE_KEY, signatureHash) putString(PKGNAME_KEY, pkgName) }) { - targetController = target + listener = target } override fun onCreateDialog(savedViewState: Bundle?): Dialog { @@ -22,10 +22,10 @@ class ExtensionTrustDialog(bundle: Bundle? = null) : DialogController(bundle) .title(R.string.untrusted_extension) .message(R.string.untrusted_extension_message) .positiveButton(R.string.ext_trust) { - (targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!) + listener.trustSignature(args.getString(SIGNATURE_KEY)!!) } .negativeButton(R.string.ext_uninstall) { - (targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!) + listener.uninstallExtension(args.getString(PKGNAME_KEY)!!) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index ca1ee029d4..762bcf79de 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -8,7 +8,6 @@ import com.bluelinelabs.conductor.Controller import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.extension.ExtensionController import eu.kanade.tachiyomi.ui.migration.MigrationController import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.openInBrowser @@ -24,13 +23,6 @@ class SettingsMainController : SettingsController() { val tintColor = context.getResourceColor(R.attr.colorAccent) - extensionPreference { - iconRes = R.drawable.ic_extension_black_24dp - iconTint = tintColor - titleRes = R.string.label_extensions - onClick { navigateTo(ExtensionController()) } - } - preference { iconRes = R.drawable.ic_tune_white_24dp iconTint = tintColor diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 1fb821f6a3..340e1751d4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -294,10 +294,12 @@ data class ViewPaddingState( ) -fun Controller.setOnQueryTextChangeListener(searchView: SearchView, f: (text: String?) -> Boolean) { +fun Controller.setOnQueryTextChangeListener(searchView: SearchView, onlyOnSubmit:Boolean = false, + f: (text: String?) -> Boolean) { searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextChange(newText: String?): Boolean { - if (router.backstack.lastOrNull()?.controller() == this@setOnQueryTextChangeListener) { + if (!onlyOnSubmit && router.backstack.lastOrNull()?.controller() == + this@setOnQueryTextChangeListener) { return f(newText) } return false @@ -324,10 +326,10 @@ fun Controller.scrollViewWith(recycler: RecyclerView, val headerHeight = insets.systemWindowInsetTop + array.getDimensionPixelSize(0, 0) view.updatePaddingRelative( top = headerHeight, - bottom = if (padBottom) insets.systemWindowInsetBottom else 0 + bottom = if (padBottom) insets.systemWindowInsetBottom else view.paddingBottom ) swipeRefreshLayout?.setProgressViewOffset(false, headerHeight + (-60).dpToPx, - headerHeight + 10.dpToPx) + headerHeight) statusBarHeight = insets.systemWindowInsetTop array.recycle() f?.invoke(insets) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ExtensionPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ExtensionPreference.kt index 3e64e84269..b57e4528df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ExtensionPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ExtensionPreference.kt @@ -28,7 +28,7 @@ class ExtensionPreference @JvmOverloads constructor(context: Context, attrs: Att val updates = Injekt.get().extensionUpdatesCount().getOrDefault() if (updates > 0) { extUpdateText.text = context.resources.getQuantityString(R.plurals - .extensions_updates_available, updates, updates) + .updates_available, updates, updates) extUpdateText.visible() } else { diff --git a/app/src/main/res/color/btn_bg_primary_selector.xml b/app/src/main/res/color/btn_bg_primary_selector.xml new file mode 100644 index 0000000000..15e0485f55 --- /dev/null +++ b/app/src/main/res/color/btn_bg_primary_selector.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml index ce8796cb79..885909db8c 100644 --- a/app/src/main/res/drawable/ic_sync_black_24dp.xml +++ b/app/src/main/res/drawable/ic_sync_black_24dp.xml @@ -2,6 +2,7 @@ android:width="24dp" android:height="24dp" android:viewportWidth="24.0" + android:tint="?actionBarTintColor" android:viewportHeight="24.0"> + + + \ No newline at end of file diff --git a/app/src/main/res/layout/catalogue_main_controller.xml b/app/src/main/res/layout/catalogue_main_controller.xml index fbdbe9b2d1..79157f0c36 100644 --- a/app/src/main/res/layout/catalogue_main_controller.xml +++ b/app/src/main/res/layout/catalogue_main_controller.xml @@ -1,15 +1,44 @@ - + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> - + android:layout_height="match_parent" + android:background="?android:attr/colorBackground"> - + + + + + + + + + + diff --git a/app/src/main/res/layout/catalouge_more_extensions_card_item.xml b/app/src/main/res/layout/catalouge_more_extensions_card_item.xml new file mode 100644 index 0000000000..855ef67e13 --- /dev/null +++ b/app/src/main/res/layout/catalouge_more_extensions_card_item.xml @@ -0,0 +1,43 @@ + + + + + + + +