From 09067cd2d4aaeb66ce448478f1da659f24bc48c3 Mon Sep 17 00:00:00 2001 From: kitsunyan Date: Thu, 30 Jul 2020 07:03:32 +0300 Subject: [PATCH] Allow to view packages per repository --- .../foxydroid/database/CursorOwner.kt | 12 +- .../kitsunyan/foxydroid/database/Database.kt | 11 +- .../kitsunyan/foxydroid/entity/ProductItem.kt | 36 +++ .../foxydroid/screen/ProductsFragment.kt | 30 +-- .../foxydroid/screen/TabsFragment.kt | 241 +++++++++++------- .../foxydroid/service/SyncService.kt | 2 +- src/main/res/layout/tabs_toolbar.xml | 8 +- src/main/res/values/ids.xml | 2 +- 8 files changed, 219 insertions(+), 123 deletions(-) diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt index 5066304..7a6dc2c 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt @@ -11,19 +11,19 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks { sealed class Request { internal abstract val id: Int - data class ProductsAvailable(val searchQuery: String, val category: String, + data class ProductsAvailable(val searchQuery: String, val section: ProductItem.Section, val order: ProductItem.Order): Request() { override val id: Int get() = 1 } - data class ProductsInstalled(val searchQuery: String, val category: String, + data class ProductsInstalled(val searchQuery: String, val section: ProductItem.Section, val order: ProductItem.Order): Request() { override val id: Int get() = 2 } - data class ProductsUpdates(val searchQuery: String, val category: String, + data class ProductsUpdates(val searchQuery: String, val section: ProductItem.Section, val order: ProductItem.Order): Request() { override val id: Int get() = 3 @@ -79,11 +79,11 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks { return QueryLoader(requireContext()) { when (request) { is Request.ProductsAvailable -> Database.ProductAdapter - .query(false, false, request.searchQuery, request.category, request.order, it) + .query(false, false, request.searchQuery, request.section, request.order, it) is Request.ProductsInstalled -> Database.ProductAdapter - .query(true, false, request.searchQuery, request.category, request.order, it) + .query(true, false, request.searchQuery, request.section, request.order, it) is Request.ProductsUpdates -> Database.ProductAdapter - .query(true, true, request.searchQuery, request.category, request.order, it) + .query(true, true, request.searchQuery, request.section, request.order, it) is Request.Repositories -> Database.RepositoryAdapter.query(it) } } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt index f044ea5..7b7a24d 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt @@ -395,7 +395,7 @@ object Database { } fun query(installed: Boolean, updates: Boolean, searchQuery: String, - category: String, order: ProductItem.Order, signal: CancellationSignal?): Cursor { + section: ProductItem.Section, order: ProductItem.Order, signal: CancellationSignal?): Cursor { val builder = QueryBuilder() val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND @@ -434,16 +434,19 @@ object Database { } builder += """JOIN ${Schema.Installed.name} AS installed ON product.${Schema.Product.ROW_PACKAGE_NAME} = installed.${Schema.Installed.ROW_PACKAGE_NAME}""" - if (category.isNotEmpty()) { + if (section is ProductItem.Section.Category) { builder += """JOIN ${Schema.Category.name} AS category ON product.${Schema.Product.ROW_PACKAGE_NAME} = category.${Schema.Product.ROW_PACKAGE_NAME}""" } builder += """WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND repository.${Schema.Repository.ROW_DELETED} == 0""" - if (category.isNotEmpty()) { + if (section is ProductItem.Section.Category) { builder += "AND category.${Schema.Category.ROW_NAME} = ?" - builder %= category + builder %= section.name + } else if (section is ProductItem.Section.Repository) { + builder += "AND product.${Schema.Product.ROW_REPOSITORY_ID} = ?" + builder %= section.id.toString() } if (searchQuery.isNotEmpty()) { builder += """AND ${Schema.Synthetic.ROW_MATCH_RANK} > 0""" diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt index a188910..8fc028b 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt @@ -1,13 +1,49 @@ package nya.kitsunyan.foxydroid.entity +import android.os.Parcel import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import nya.kitsunyan.foxydroid.R +import nya.kitsunyan.foxydroid.utility.KParcelable import nya.kitsunyan.foxydroid.utility.extension.json.* data class ProductItem(val repositoryId: Long, val packageName: String, val name: String, val summary: String, val icon: String, val version: String, val installedVersion: String, val compatible: Boolean, val canUpdate: Boolean, val matchRank: Int) { + sealed class Section: KParcelable { + object All: Section() { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { All } + } + + data class Category(val name: String): Section() { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(name) + } + + companion object { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { + val name = it.readString()!! + Category(name) + } + } + } + + data class Repository(val id: Long, val name: String): Section() { + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeLong(id) + dest.writeString(name) + } + + companion object { + @Suppress("unused") @JvmField val CREATOR = KParcelable.creator { + val id = it.readLong() + val name = it.readString()!! + Repository(id, name) + } + } + } + } + enum class Order(val titleResId: Int) { NAME(R.string.name), DATE_ADDED(R.string.date_added), diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt index e81f783..7af8a19 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt @@ -25,12 +25,12 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { private const val EXTRA_SOURCE = "source" private const val STATE_CURRENT_SEARCH_QUERY = "currentSearchQuery" - private const val STATE_CURRENT_CATEGORY = "currentCategory" + private const val STATE_CURRENT_SECTION = "currentSection" private const val STATE_CURRENT_ORDER = "currentOrder" private const val STATE_LAYOUT_MANAGER = "layoutManager" } - enum class Source(val titleResId: Int, val categories: Boolean, val order: Boolean) { + enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean) { AVAILABLE(R.string.available, true, true), INSTALLED(R.string.installed, false, false), UPDATES(R.string.updates, false, false) @@ -46,11 +46,11 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf) private var searchQuery = "" - private var category = "" + private var section: ProductItem.Section = ProductItem.Section.All private var order = ProductItem.Order.NAME private var currentSearchQuery = "" - private var currentCategory = "" + private var currentSection: ProductItem.Section = ProductItem.Section.All private var currentOrder = ProductItem.Order.NAME private var layoutManagerState: Parcelable? = null @@ -61,12 +61,12 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { private val request: CursorOwner.Request get() { val searchQuery = searchQuery - val category = if (source.categories) category else "" + val section = if (source.sections) section else ProductItem.Section.All val order = if (source.order) order else ProductItem.Order.NAME return when (source) { - Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, category, order) - Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, category, order) - Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, category, order) + Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, section, order) + Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, section, order) + Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, section, order) } } @@ -90,7 +90,7 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { super.onViewCreated(view, savedInstanceState) currentSearchQuery = savedInstanceState?.getString(STATE_CURRENT_SEARCH_QUERY).orEmpty() - currentCategory = savedInstanceState?.getString(STATE_CURRENT_CATEGORY).orEmpty() + currentSection = savedInstanceState?.getParcelable(STATE_CURRENT_SECTION) ?: ProductItem.Section.All currentOrder = savedInstanceState?.getString(STATE_CURRENT_ORDER) ?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) @@ -119,7 +119,7 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { super.onSaveInstanceState(outState) outState.putString(STATE_CURRENT_SEARCH_QUERY, currentSearchQuery) - outState.putString(STATE_CURRENT_CATEGORY, currentCategory) + outState.putParcelable(STATE_CURRENT_SECTION, currentSection) outState.putString(STATE_CURRENT_ORDER, currentOrder.name) (layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()) ?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) } @@ -144,9 +144,9 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { recyclerView?.layoutManager?.onRestoreInstanceState(it) } - if (currentSearchQuery != searchQuery || currentCategory != category || currentOrder != order) { + if (currentSearchQuery != searchQuery || currentSection != section || currentOrder != order) { currentSearchQuery = searchQuery - currentCategory = category + currentSection = section currentOrder = order recyclerView?.scrollToPosition(0) } @@ -161,9 +161,9 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback { } } - internal fun setCategory(category: String) { - if (this.category != category) { - this.category = category + internal fun setSection(section: ProductItem.Section) { + if (this.section != section) { + this.section = section if (view != null) { screenActivity.cursorOwner.attach(this, request) } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt index dc9d7cc..559d798 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt @@ -34,13 +34,14 @@ import io.reactivex.rxjava3.schedulers.Schedulers import nya.kitsunyan.foxydroid.R import nya.kitsunyan.foxydroid.content.Preferences import nya.kitsunyan.foxydroid.database.Database +import nya.kitsunyan.foxydroid.entity.ProductItem import nya.kitsunyan.foxydroid.service.Connection import nya.kitsunyan.foxydroid.service.SyncService import nya.kitsunyan.foxydroid.utility.RxUtils import nya.kitsunyan.foxydroid.utility.Utils import nya.kitsunyan.foxydroid.utility.extension.android.* import nya.kitsunyan.foxydroid.utility.extension.resources.* -import nya.kitsunyan.foxydroid.utility.extension.text.* +import nya.kitsunyan.foxydroid.widget.DividerItemDecoration import nya.kitsunyan.foxydroid.widget.EnumRecyclerAdapter import nya.kitsunyan.foxydroid.widget.FocusSearchView import kotlin.math.* @@ -49,43 +50,43 @@ class TabsFragment: ScreenFragment() { companion object { private const val STATE_SEARCH_FOCUSED = "searchFocused" private const val STATE_SEARCH_QUERY = "searchQuery" - private const val STATE_SHOW_CATEGORIES = "showCategories" - private const val STATE_CATEGORIES = "categories" - private const val STATE_CATEGORY = "category" + private const val STATE_SHOW_SECTIONS = "showSections" + private const val STATE_SECTIONS = "sections" + private const val STATE_SECTION = "section" } private class Layout(view: View) { val tabs = view.findViewById(R.id.tabs)!! - val categoryLayout = view.findViewById(R.id.category_layout)!! - val categoryChange = view.findViewById(R.id.category_change)!! - val categoryName = view.findViewById(R.id.category_name)!! - val categoryIcon = view.findViewById(R.id.category_icon)!! + val sectionLayout = view.findViewById(R.id.section_layout)!! + val sectionChange = view.findViewById(R.id.section_change)!! + val sectionName = view.findViewById(R.id.section_name)!! + val sectionIcon = view.findViewById(R.id.section_icon)!! } private var searchMenuItem: MenuItem? = null private var sortOrderMenu: Pair>? = null private var syncRepositoriesMenuItem: MenuItem? = null private var layout: Layout? = null - private var categoriesList: RecyclerView? = null + private var sectionsList: RecyclerView? = null private var viewPager: ViewPager2? = null - private var showCategories = false + private var showSections = false set(value) { if (field != value) { field = value val layout = layout layout?.tabs?.let { (0 until it.childCount) .forEach { index -> it.getChildAt(index)!!.isEnabled = !value } } - layout?.categoryIcon?.scaleY = if (value) -1f else 1f - if ((categoriesList?.parent as? View)?.height ?: 0 > 0) { - animateCategoriesList() + layout?.sectionIcon?.scaleY = if (value) -1f else 1f + if ((sectionsList?.parent as? View)?.height ?: 0 > 0) { + animateSectionsList() } } } private var searchQuery = "" - private var categories = emptyList() - private var category = "" + private var sections = listOf(ProductItem.Section.All) + private var section: ProductItem.Section = ProductItem.Section.All private val syncConnection = Connection(SyncService::class.java, onBind = { _, _ -> viewPager?.let { @@ -96,7 +97,8 @@ class TabsFragment: ScreenFragment() { private var sortOrderDisposable: Disposable? = null private var categoriesDisposable: Disposable? = null - private var categoriesAnimator: ValueAnimator? = null + private var repositoriesDisposable: Disposable? = null + private var sectionsAnimator: ValueAnimator? = null private var needSelectUpdates = false @@ -212,10 +214,11 @@ class TabsFragment: ScreenFragment() { (tab.layoutParams as LinearLayout.LayoutParams).weight = 1f } - showCategories = savedInstanceState?.getByte(STATE_SHOW_CATEGORIES)?.toInt() ?: 0 != 0 - categories = savedInstanceState?.getStringArrayList(STATE_CATEGORIES).orEmpty() - category = savedInstanceState?.getString(STATE_CATEGORY).orEmpty() - layout.categoryChange.setOnClickListener { showCategories = categories.isNotEmpty() && !showCategories } + showSections = savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0 != 0 + sections = savedInstanceState?.getParcelableArrayList(STATE_SECTIONS).orEmpty() + section = savedInstanceState?.getParcelable(STATE_SECTION) ?: ProductItem.Section.All + layout.sectionChange.setOnClickListener { showSections = sections + .any { it !is ProductItem.Section.All } && !showSections } updateOrder() sortOrderDisposable = Preferences.observable.subscribe { @@ -243,34 +246,38 @@ class TabsFragment: ScreenFragment() { .observeOn(Schedulers.io()) .flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } } .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - val categories = it.sorted() - if (this.categories != categories) { - this.categories = categories - updateCategory() - } - } - updateCategory() + .subscribe { setSectionsAndUpdate(it.asSequence().sorted() + .map(ProductItem.Section::Category).toList(), null) } + repositoriesDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { setSectionsAndUpdate(null, it.asSequence().filter { it.enabled } + .map { ProductItem.Section.Repository(it.id, it.name) }.toList()) } + updateSection() - val categoriesList = RecyclerView(toolbar.context).apply { - id = R.id.categories_list + val sectionsList = RecyclerView(toolbar.context).apply { + id = R.id.sections_list layoutManager = LinearLayoutManager(context) isMotionEventSplittingEnabled = false isVerticalScrollBarEnabled = false setHasFixedSize(true) - this.adapter = CategoriesAdapter({ categories }) { - if (showCategories) { - showCategories = false - category = it - updateCategory() + val adapter = SectionsAdapter({ sections }) { + if (showSections) { + showSections = false + section = it + updateSection() } } + this.adapter = adapter + addItemDecoration(DividerItemDecoration(context, adapter::configureDivider)) setBackgroundColor(context.getColorFromAttr(android.R.attr.colorPrimaryDark).defaultColor) elevation = resources.sizeScaled(4).toFloat() content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, 0) visibility = View.GONE } - this.categoriesList = categoriesList + this.sectionsList = sectionsList var lastContentHeight = -1 content.viewTreeObserver.addOnGlobalLayoutListener { @@ -280,11 +287,11 @@ class TabsFragment: ScreenFragment() { if (lastContentHeight != contentHeight) { lastContentHeight = contentHeight if (initial) { - categoriesList.layoutParams.height = if (showCategories) contentHeight else 0 - categoriesList.visibility = if (showCategories) View.VISIBLE else View.GONE - categoriesList.requestLayout() + sectionsList.layoutParams.height = if (showSections) contentHeight else 0 + sectionsList.visibility = if (showSections) View.VISIBLE else View.GONE + sectionsList.requestLayout() } else { - animateCategoriesList() + animateSectionsList() } } } @@ -298,7 +305,7 @@ class TabsFragment: ScreenFragment() { sortOrderMenu = null syncRepositoriesMenuItem = null layout = null - categoriesList = null + sectionsList = null viewPager = null syncConnection.unbind(requireContext()) @@ -306,8 +313,10 @@ class TabsFragment: ScreenFragment() { sortOrderDisposable = null categoriesDisposable?.dispose() categoriesDisposable = null - categoriesAnimator?.cancel() - categoriesAnimator = null + repositoriesDisposable?.dispose() + repositoriesDisposable = null + sectionsAnimator?.cancel() + sectionsAnimator = null } override fun onSaveInstanceState(outState: Bundle) { @@ -315,9 +324,9 @@ class TabsFragment: ScreenFragment() { outState.putBoolean(STATE_SEARCH_FOCUSED, searchMenuItem?.actionView!!.hasFocus()) outState.putString(STATE_SEARCH_QUERY, searchQuery) - outState.putByte(STATE_SHOW_CATEGORIES, if (showCategories) 1 else 0) - outState.putStringArrayList(STATE_CATEGORIES, ArrayList(categories)) - outState.putString(STATE_CATEGORY, category) + outState.putByte(STATE_SHOW_SECTIONS, if (showSections) 1 else 0) + outState.putParcelableArrayList(STATE_SECTIONS, ArrayList(sections)) + outState.putParcelable(STATE_SECTION, section) } override fun onViewStateRestored(savedInstanceState: Bundle?) { @@ -335,7 +344,7 @@ class TabsFragment: ScreenFragment() { if (view != null && childFragment is ProductsFragment) { childFragment.setSearchQuery(searchQuery) - childFragment.setCategory(category) + childFragment.setSection(section) childFragment.setOrder(Preferences[Preferences.Key.SortOrder].order) } } @@ -346,8 +355,8 @@ class TabsFragment: ScreenFragment() { searchMenuItem?.collapseActionView() true } - showCategories -> { - showCategories = false + showSections -> { + showSections = false true } else -> { @@ -387,31 +396,52 @@ class TabsFragment: ScreenFragment() { productFragments.forEach { it.setOrder(order) } } - private fun updateCategory() { - if (category.isNotEmpty() && categories.indexOf(category) < 0) { - category = "" - } - layout?.categoryName?.text = category.nullIfEmpty() ?: getString(R.string.all_applications) - layout?.categoryIcon?.visibility = if (categories.isEmpty()) View.GONE else View.VISIBLE - productFragments.forEach { it.setCategory(category) } - categoriesList?.adapter?.notifyDataSetChanged() + private inline fun collectOldSections(list: List?): List? { + val oldList = sections.mapNotNull { it as? T } + return if (list == null || oldList == list) oldList else null } - private fun animateCategoriesList() { - val categoriesList = categoriesList!! - val value = if (categoriesList.visibility != View.VISIBLE) 0f else - categoriesList.height.toFloat() / (categoriesList.parent as View).height - val target = if (showCategories) 1f else 0f - categoriesAnimator?.cancel() - categoriesAnimator = null + private fun setSectionsAndUpdate(categories: List?, + repositories: List?) { + val oldCategories = collectOldSections(categories) + val oldRepositories = collectOldSections(repositories) + if (oldCategories == null || oldRepositories == null) { + sections = listOf(ProductItem.Section.All) + + (categories ?: oldCategories).orEmpty() + + (repositories ?: oldRepositories).orEmpty() + updateSection() + } + } + + private fun updateSection() { + if (section !in sections) { + section = ProductItem.Section.All + } + layout?.sectionName?.text = when (val section = section) { + is ProductItem.Section.All -> getString(R.string.all_applications) + is ProductItem.Section.Category -> section.name + is ProductItem.Section.Repository -> section.name + } + layout?.sectionIcon?.visibility = if (sections.any { it !is ProductItem.Section.All }) View.VISIBLE else View.GONE + productFragments.forEach { it.setSection(section) } + sectionsList?.adapter?.notifyDataSetChanged() + } + + private fun animateSectionsList() { + val sectionsList = sectionsList!! + val value = if (sectionsList.visibility != View.VISIBLE) 0f else + sectionsList.height.toFloat() / (sectionsList.parent as View).height + val target = if (showSections) 1f else 0f + sectionsAnimator?.cancel() + sectionsAnimator = null if (value != target) { - categoriesAnimator = ValueAnimator.ofFloat(value, target).apply { + sectionsAnimator = ValueAnimator.ofFloat(value, target).apply { duration = (250 * abs(target - value)).toLong() interpolator = if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f) addUpdateListener { val newValue = animatedValue as Float - categoriesList.apply { + sectionsList.apply { val height = ((parent as View).height * newValue).toInt() val visible = height > 0 if ((visibility == View.VISIBLE) != visible) { @@ -423,7 +453,7 @@ class TabsFragment: ScreenFragment() { } } if (target <= 0f && newValue <= 0f || target >= 1f && newValue >= 1f) { - categoriesAnimator = null + sectionsAnimator = null } } start() @@ -434,24 +464,24 @@ class TabsFragment: ScreenFragment() { private val pageChangeCallback = object: ViewPager2.OnPageChangeCallback() { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { val layout = layout!! - val fromCategories = ProductsFragment.Source.values()[position].categories - val toCategories = if (positionOffset <= 0f) fromCategories else - ProductsFragment.Source.values()[position + 1].categories - val offset = if (fromCategories != toCategories) { - if (fromCategories) 1f - positionOffset else positionOffset + val fromSections = ProductsFragment.Source.values()[position].sections + val toSections = if (positionOffset <= 0f) fromSections else + ProductsFragment.Source.values()[position + 1].sections + val offset = if (fromSections != toSections) { + if (fromSections) 1f - positionOffset else positionOffset } else { - if (fromCategories) 1f else 0f + if (fromSections) 1f else 0f } (layout.tabs.background as TabsBackgroundDrawable) .update(position + positionOffset, layout.tabs.childCount) - assert(layout.categoryLayout.childCount == 1) - val child = layout.categoryLayout.getChildAt(0) + assert(layout.sectionLayout.childCount == 1) + val child = layout.sectionLayout.getChildAt(0) val height = child.layoutParams.height assert(height > 0) val currentHeight = (offset * height).roundToInt() - if (layout.categoryLayout.layoutParams.height != currentHeight) { - layout.categoryLayout.layoutParams.height = currentHeight - layout.categoryLayout.requestLayout() + if (layout.sectionLayout.layoutParams.height != currentHeight) { + layout.sectionLayout.layoutParams.height = currentHeight + layout.sectionLayout.requestLayout() } } @@ -462,14 +492,14 @@ class TabsFragment: ScreenFragment() { syncRepositoriesMenuItem!!.setShowAsActionFlags(if (!source.order || resources.configuration.screenWidthDp >= 480) MenuItem.SHOW_AS_ACTION_ALWAYS else 0) setSelectedTab(source) - if (showCategories && !source.categories) { - showCategories = false + if (showSections && !source.sections) { + showSections = false } } override fun onPageScrollStateChanged(state: Int) { val source = ProductsFragment.Source.values()[viewPager!!.currentItem] - layout!!.categoryChange.isEnabled = state != ViewPager2.SCROLL_STATE_DRAGGING && source.categories + layout!!.sectionChange.isEnabled = state != ViewPager2.SCROLL_STATE_DRAGGING && source.sections if (state == ViewPager2.SCROLL_STATE_IDLE) { // onPageSelected can be called earlier than fragments created updateUpdateNotificationBlocker(source) @@ -512,11 +542,12 @@ class TabsFragment: ScreenFragment() { override fun getOpacity(): Int = PixelFormat.TRANSLUCENT } - private class CategoriesAdapter(private val categories: () -> List, private val onClick: (String) -> Unit): - EnumRecyclerAdapter() { - enum class ViewType { CATEGORY } + private class SectionsAdapter(private val sections: () -> List, + private val onClick: (ProductItem.Section) -> Unit): EnumRecyclerAdapter() { + enum class ViewType { SECTION } - private class CategoryViewHolder(context: Context): RecyclerView.ViewHolder(TextView(context)) { + private class SectionViewHolder(context: Context): RecyclerView.ViewHolder(TextView(context)) { val title: TextView get() = itemView as TextView @@ -532,22 +563,48 @@ class TabsFragment: ScreenFragment() { } } + fun configureDivider(context: Context, position: Int, configuration: DividerItemDecoration.Configuration) { + val currentSection = sections()[position] + val nextSection = sections().getOrNull(position + 1) + when { + nextSection != null && currentSection.javaClass != nextSection.javaClass -> { + val padding = context.resources.sizeScaled(16) + configuration.set(true, false, padding, padding) + } + else -> { + configuration.set(false, false, 0, 0) + } + } + } + override val viewTypeClass: Class get() = ViewType::class.java - override fun getItemCount(): Int = 1 + categories().size - override fun getItemEnumViewType(position: Int): ViewType = ViewType.CATEGORY + override fun getItemCount(): Int = sections().size + override fun getItemEnumViewType(position: Int): ViewType = ViewType.SECTION override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder { - return CategoryViewHolder(parent.context).apply { - itemView.setOnClickListener { onClick(categories().getOrNull(adapterPosition - 1).orEmpty()) } + return SectionViewHolder(parent.context).apply { + itemView.setOnClickListener { onClick(sections()[adapterPosition]) } } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - holder as CategoryViewHolder - holder.title.text = categories().getOrNull(position - 1) - ?: holder.itemView.resources.getString(R.string.all_applications) + holder as SectionViewHolder + val section = sections()[position] + val previousSection = sections().getOrNull(position - 1) + val nextSection = sections().getOrNull(position + 1) + val margin = holder.itemView.resources.sizeScaled(8) + val layoutParams = holder.itemView.layoutParams as RecyclerView.LayoutParams + layoutParams.topMargin = if (previousSection == null || + section.javaClass != previousSection.javaClass) margin else 0 + layoutParams.bottomMargin = if (nextSection == null || + section.javaClass != nextSection.javaClass) margin else 0 + holder.title.text = when (section) { + is ProductItem.Section.All -> holder.itemView.resources.getString(R.string.all_applications) + is ProductItem.Section.Category -> section.name + is ProductItem.Section.Repository -> section.name + } } } } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/service/SyncService.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/service/SyncService.kt index dd23527..037ea6a 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/service/SyncService.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/service/SyncService.kt @@ -316,7 +316,7 @@ class SyncService: ConnectionService() { if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) { val disposable = RxUtils .querySingle { Database.ProductAdapter - .query(true, true, "", "", ProductItem.Order.NAME, it) + .query(true, true, "", ProductItem.Section.All, ProductItem.Order.NAME, it) .use { it.asSequence().map(Database.ProductAdapter::transformItem).toList() } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/src/main/res/layout/tabs_toolbar.xml b/src/main/res/layout/tabs_toolbar.xml index 5f7b8a6..7fd12df 100644 --- a/src/main/res/layout/tabs_toolbar.xml +++ b/src/main/res/layout/tabs_toolbar.xml @@ -13,12 +13,12 @@ android:orientation="horizontal" /> - +