From 561352b685dce760e6fad9fd4cca8d92af5b344a Mon Sep 17 00:00:00 2001 From: kitsunyan Date: Tue, 16 Jun 2020 12:01:53 +0300 Subject: [PATCH] Allow to sort by date added and last update --- .../foxydroid/database/CursorOwner.kt | 16 +- .../kitsunyan/foxydroid/database/Database.kt | 16 +- .../kitsunyan/foxydroid/entity/ProductItem.kt | 7 + .../foxydroid/screen/ProductsFragment.kt | 34 ++- .../foxydroid/screen/RepositoryFragment.kt | 2 +- .../foxydroid/screen/TabsFragment.kt | 254 +++++++++++------- .../foxydroid/service/SyncService.kt | 2 +- src/main/res/drawable/ic_sort.xml | 13 + src/main/res/layout/fragment.xml | 4 +- src/main/res/values/strings.xml | 4 +- src/main/res/values/styles.xml | 4 + 11 files changed, 232 insertions(+), 124 deletions(-) create mode 100644 src/main/res/drawable/ic_sort.xml diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt index 7477994..5066304 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/database/CursorOwner.kt @@ -5,22 +5,26 @@ import android.os.Bundle import androidx.fragment.app.Fragment import androidx.loader.app.LoaderManager import androidx.loader.content.Loader +import nya.kitsunyan.foxydroid.entity.ProductItem class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks { sealed class Request { internal abstract val id: Int - data class ProductsAvailable(val searchQuery: String, val category: String): Request() { + data class ProductsAvailable(val searchQuery: String, val category: String, + val order: ProductItem.Order): Request() { override val id: Int get() = 1 } - data class ProductsInstalled(val searchQuery: String, val category: String): Request() { + data class ProductsInstalled(val searchQuery: String, val category: String, + val order: ProductItem.Order): Request() { override val id: Int get() = 2 } - data class ProductsUpdates(val searchQuery: String, val category: String): Request() { + data class ProductsUpdates(val searchQuery: String, val category: String, + val order: ProductItem.Order): Request() { override val id: Int get() = 3 } @@ -75,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, it) + .query(false, false, request.searchQuery, request.category, request.order, it) is Request.ProductsInstalled -> Database.ProductAdapter - .query(true, false, request.searchQuery, request.category, it) + .query(true, false, request.searchQuery, request.category, request.order, it) is Request.ProductsUpdates -> Database.ProductAdapter - .query(true, true, request.searchQuery, request.category, it) + .query(true, true, request.searchQuery, request.category, 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 f2017db..ecff14e 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/database/Database.kt @@ -75,6 +75,8 @@ object Database { const val ROW_PACKAGE_NAME = "package_name" const val ROW_NAME = "name" const val ROW_SUMMARY = "summary" + const val ROW_ADDED = "added" + const val ROW_UPDATED = "updated" const val ROW_VERSION_CODE = "version_code" const val ROW_SIGNATURE = "signature" const val ROW_COMPATIBLE = "compatible" @@ -88,6 +90,8 @@ object Database { $ROW_PACKAGE_NAME TEXT NOT NULL, $ROW_NAME TEXT NOT NULL, $ROW_SUMMARY TEXT NOT NULL, + $ROW_ADDED INTEGER NOT NULL, + $ROW_UPDATED INTEGER NOT NULL, $ROW_VERSION_CODE INTEGER NOT NULL, $ROW_SIGNATURE TEXT NOT NULL, $ROW_COMPATIBLE INTEGER NOT NULL, @@ -386,7 +390,7 @@ object Database { } fun query(installed: Boolean, updates: Boolean, searchQuery: String, - category: String, signal: CancellationSignal?): Cursor { + category: String, order: ProductItem.Order, signal: CancellationSignal?): Cursor { val builder = QueryBuilder() builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID}, @@ -432,7 +436,13 @@ object Database { if (updates) { builder += "AND ${Schema.Synthetic.ROW_CAN_UPDATE}" } - builder += "ORDER BY product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC" + builder += "ORDER BY" + when (order) { + ProductItem.Order.NAME -> Unit + ProductItem.Order.DATE_ADDED -> builder += "product.${Schema.Product.ROW_ADDED} DESC," + ProductItem.Order.LAST_UPDATE -> builder += "product.${Schema.Product.ROW_UPDATED} DESC," + }::class + builder += "product.${Schema.Product.ROW_NAME} COLLATE LOCALIZED ASC" return builder.query(db, signal).observable(Subject.Products) } @@ -569,6 +579,8 @@ object Database { put(Schema.Product.ROW_PACKAGE_NAME, product.packageName) put(Schema.Product.ROW_NAME, product.name) put(Schema.Product.ROW_SUMMARY, product.summary) + put(Schema.Product.ROW_ADDED, product.added) + put(Schema.Product.ROW_UPDATED, product.updated) put(Schema.Product.ROW_VERSION_CODE, product.versionCode) put(Schema.Product.ROW_SIGNATURE, product.signature) put(Schema.Product.ROW_COMPATIBLE, if (product.compatible) 1 else 0) diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt index d198247..1cad3f5 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/entity/ProductItem.kt @@ -2,11 +2,18 @@ package nya.kitsunyan.foxydroid.entity import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser +import nya.kitsunyan.foxydroid.R 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) { + enum class Order(val titleResId: Int) { + NAME(R.string.name), + DATE_ADDED(R.string.date_added), + LAST_UPDATE(R.string.last_update) + } + fun serialize(generator: JsonGenerator) { generator.writeNumberField("serialVersion", 1) generator.writeStringField("icon", icon) diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt index 699fe70..0281ec0 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/ProductsFragment.kt @@ -16,6 +16,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers import nya.kitsunyan.foxydroid.R import nya.kitsunyan.foxydroid.database.CursorOwner import nya.kitsunyan.foxydroid.database.Database +import nya.kitsunyan.foxydroid.entity.ProductItem import nya.kitsunyan.foxydroid.utility.RxUtils import nya.kitsunyan.foxydroid.utility.extension.resources.* import nya.kitsunyan.foxydroid.widget.DividerItemDecoration @@ -27,13 +28,14 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback { private const val STATE_CURRENT_SEARCH_QUERY = "currentSearchQuery" private const val STATE_CURRENT_CATEGORY = "currentCategory" + private const val STATE_CURRENT_ORDER = "currentOrder" private const val STATE_LAYOUT_MANAGER = "layoutManager" } - enum class Source(val titleResId: Int, val categories: Boolean) { - AVAILABLE(R.string.available, true), - INSTALLED(R.string.installed, false), - UPDATES(R.string.updates, false) + enum class Source(val titleResId: Int, val categories: Boolean, val order: Boolean) { + AVAILABLE(R.string.available, true, true), + INSTALLED(R.string.installed, false, false), + UPDATES(R.string.updates, false, false) } constructor(source: Source): this() { @@ -47,9 +49,11 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback { private var searchQuery = "" private var category = "" + private var order = ProductItem.Order.NAME private var currentSearchQuery = "" private var currentCategory = "" + private var currentOrder = ProductItem.Order.NAME private var layoutManagerState: Parcelable? = null private var recyclerView: RecyclerView? = null @@ -60,10 +64,11 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback { get() { val searchQuery = searchQuery val category = if (source.categories) category else "" + val order = if (source.order) order else ProductItem.Order.NAME return when (source) { - Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, category) - Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, category) - Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, category) + Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, category, order) + Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, category, order) + Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, category, order) } } @@ -87,6 +92,8 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback { currentSearchQuery = savedInstanceState?.getString(STATE_CURRENT_SEARCH_QUERY).orEmpty() currentCategory = savedInstanceState?.getString(STATE_CURRENT_CATEGORY).orEmpty() + currentOrder = savedInstanceState?.getString(STATE_CURRENT_ORDER) + ?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) screenActivity.cursorOwner.attach(this, request) @@ -114,6 +121,7 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback { outState.putString(STATE_CURRENT_SEARCH_QUERY, currentSearchQuery) outState.putString(STATE_CURRENT_CATEGORY, currentCategory) + outState.putString(STATE_CURRENT_ORDER, currentOrder.name) (layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()) ?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) } } @@ -137,9 +145,10 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback { recyclerView?.layoutManager?.onRestoreInstanceState(it) } - if (currentSearchQuery != searchQuery || currentCategory != category) { + if (currentSearchQuery != searchQuery || currentCategory != category || currentOrder != order) { currentSearchQuery = searchQuery currentCategory = category + currentOrder = order recyclerView?.scrollToPosition(0) } } @@ -161,4 +170,13 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback { } } } + + internal fun setOrder(order: ProductItem.Order) { + if (this.order != order) { + this.order = order + if (view != null) { + screenActivity.cursorOwner.attach(this, request) + } + } + } } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/RepositoryFragment.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/RepositoryFragment.kt index 0e0935b..c05d2b7 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/RepositoryFragment.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/RepositoryFragment.kt @@ -124,7 +124,7 @@ class RepositoryFragment(): Fragment() { getString(R.string.unknown) } }) - if (repository.enabled && repository.entityTag.isNotEmpty()) { + if (repository.enabled && (repository.lastModified.isNotEmpty() || repository.entityTag.isNotEmpty())) { layout.addTitleText(R.string.number_of_applications, Database.ProductAdapter.getCount(repository.id).toString()) } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt index 916cd4a..6586dd9 100644 --- a/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt +++ b/src/main/kotlin/nya/kitsunyan/foxydroid/screen/TabsFragment.kt @@ -1,6 +1,7 @@ package nya.kitsunyan.foxydroid.screen import android.animation.ValueAnimator +import android.content.Context import android.os.Bundle import android.view.Gravity import android.view.LayoutInflater @@ -15,6 +16,7 @@ import android.widget.TextView import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.Toolbar +import androidx.core.view.MenuCompat import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentStatePagerAdapter import androidx.recyclerview.widget.LinearLayoutManager @@ -27,11 +29,14 @@ import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers import nya.kitsunyan.foxydroid.R 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.resources.* +import nya.kitsunyan.foxydroid.utility.extension.text.* +import nya.kitsunyan.foxydroid.widget.EnumRecyclerAdapter import kotlin.math.* class TabsFragment: Fragment() { @@ -40,11 +45,19 @@ class TabsFragment: Fragment() { private const val STATE_SHOW_CATEGORIES = "showCategories" private const val STATE_CATEGORIES = "categories" private const val STATE_CATEGORY = "category" + private const val STATE_ORDER = "order" } - private var tabLayout: TabLayout? = null - private var categoryName: TextView? = null - private var categoryIcon: ImageView? = null + private class Layout(view: View) { + val tabLayout = 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)!! + } + + private var sortOrderMenu: Pair>? = null + private var layout: Layout? = null private var categoriesList: RecyclerView? = null private var viewPager: ViewPager? = null @@ -52,9 +65,10 @@ class TabsFragment: Fragment() { set(value) { if (field != value) { field = value - tabLayout?.let { (0 until it.tabCount) + val layout = layout + layout?.tabLayout?.let { (0 until it.tabCount) .forEach { index -> it.getTabAt(index)!!.view.isEnabled = !value } } - categoryIcon?.scaleY = if (value) -1f else 1f + layout?.categoryIcon?.scaleY = if (value) -1f else 1f if ((categoriesList?.parent as? View)?.height ?: 0 > 0) { animateCategoriesList() } @@ -64,6 +78,7 @@ class TabsFragment: Fragment() { private var searchQuery = "" private var categories = emptyList() private var category = "" + private var order = ProductItem.Order.NAME private val syncConnection = Connection(SyncService::class.java, onBind = { viewPager?.let { @@ -112,26 +127,44 @@ class TabsFragment: Fragment() { }) toolbar.menu.apply { + MenuCompat.setGroupDividerEnabled(this, true) + add(0, R.id.toolbar_search, 0, R.string.search) .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_search)) .setActionView(searchView) .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) - add(R.string.sync_repositories) + sortOrderMenu = addSubMenu(0, 0, 0, R.string.sort_order) + .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_sort)) + .let { menu -> + menu.item.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) + val items = ProductItem.Order.values().map { order -> menu + .add(order.titleResId) + .setOnMenuItemClickListener { item -> + this@TabsFragment.order = order + item.isChecked = true + productFragments.forEach { it.setOrder(order) } + true + } } + menu.setGroupCheckable(0, true, true) + Pair(menu.item, items) + } + + add(0, 0, 0, R.string.sync_repositories) .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_sync)) - .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) + .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM) .setOnMenuItemClickListener { syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL) true } - add(R.string.repositories) + add(1, 0, 0, R.string.repositories) .setOnMenuItemClickListener { view.post { screenActivity.navigateRepositories() } true } - add(R.string.preferences) + add(1, 0, 0, R.string.preferences) .setOnMenuItemClickListener { view.post { screenActivity.navigatePreferences() } true @@ -143,11 +176,12 @@ class TabsFragment: Fragment() { val toolbarExtra = view.findViewById(R.id.toolbar_extra) toolbarExtra.addView(toolbarExtra.inflate(R.layout.tabs_toolbar)) + val layout = Layout(view) + this.layout = layout - val tabLayout = view.findViewById(R.id.tabs) - this.tabLayout = tabLayout - ProductsFragment.Source.values().forEach { tabLayout.addTab(tabLayout.newTab().setText(it.titleResId)) } - tabLayout.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener { + ProductsFragment.Source.values().forEach { layout.tabLayout + .addTab(layout.tabLayout.newTab().setText(it.titleResId)) } + layout.tabLayout.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab) { viewPager!!.currentItem = tab.position } @@ -156,16 +190,14 @@ class TabsFragment: Fragment() { override fun onTabReselected(tab: TabLayout.Tab) = Unit }) - 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) - this.categoryName = categoryName - this.categoryIcon = categoryIcon showCategories = savedInstanceState?.getByte(STATE_SHOW_CATEGORIES)?.toInt() ?: 0 != 0 categories = savedInstanceState?.getStringArrayList(STATE_CATEGORIES).orEmpty() category = savedInstanceState?.getString(STATE_CATEGORY).orEmpty() - categoryChange.setOnClickListener { showCategories = categories.isNotEmpty() && !showCategories } + layout.categoryChange.setOnClickListener { showCategories = categories.isNotEmpty() && !showCategories } + + order = savedInstanceState?.getString(STATE_ORDER)?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME + sortOrderMenu!!.second[order.ordinal].isChecked = true + productFragments.forEach { it.setOrder(order) } val content = view.findViewById(R.id.fragment_content) @@ -176,45 +208,7 @@ class TabsFragment: Fragment() { override fun getCount(): Int = ProductsFragment.Source.values().size } content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) - addOnPageChangeListener(object: ViewPager.OnPageChangeListener { - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { - 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 - } else { - if (fromCategories) 1f else 0f - } - if (categoryLayout.childCount != 1) { - throw RuntimeException() - } - val child = categoryLayout.getChildAt(0) - val height = child.layoutParams.height - if (height <= 0) { - throw RuntimeException() - } - val currentHeight = (offset * height).roundToInt() - if (categoryLayout.layoutParams.height != currentHeight) { - categoryLayout.layoutParams.height = currentHeight - categoryLayout.requestLayout() - } - } - - override fun onPageSelected(position: Int) { - val source = ProductsFragment.Source.values()[position] - updateUpdateNotificationBlocker(source) - tabLayout.selectTab(tabLayout.getTabAt(source.ordinal)) - if (showCategories && !source.categories) { - showCategories = false - } - } - - override fun onPageScrollStateChanged(state: Int) { - categoryChange.isEnabled = state != ViewPager.SCROLL_STATE_DRAGGING && - ProductsFragment.Source.values()[this@apply.currentItem].categories - } - }) + addOnPageChangeListener(pageChangeListener) } categoriesDisposable = Observable.just(Unit) @@ -237,34 +231,11 @@ class TabsFragment: Fragment() { isMotionEventSplittingEnabled = false isVerticalScrollBarEnabled = false setHasFixedSize(true) - adapter = object: RecyclerView.Adapter() { - override fun getItemCount(): Int = categories.size + 1 - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return object: RecyclerView.ViewHolder(AppCompatTextView(parent.context).apply { - layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, - resources.sizeScaled(48)) - gravity = Gravity.CENTER_VERTICAL - setPadding(resources.sizeScaled(16), 0, resources.sizeScaled(16), 0) - setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary)) - background = context.getDrawableFromAttr(R.attr.selectableItemBackground) - setTextSizeScaled(16) - }) { - init { - itemView.setOnClickListener { - if (showCategories) { - showCategories = false - category = if (adapterPosition == 0) "" else categories[adapterPosition - 1] - updateCategory() - } - } - } - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder.itemView as TextView).text = if (position == 0) - getString(R.string.all_applications_category) else categories[position - 1] + this.adapter = CategoriesAdapter({ categories }) { + if (showCategories) { + showCategories = false + category = it + updateCategory() } } setBackgroundColor(context.getColorFromAttr(R.attr.colorPrimaryDark).defaultColor) @@ -296,9 +267,8 @@ class TabsFragment: Fragment() { override fun onDestroyView() { super.onDestroyView() - tabLayout = null - categoryName = null - categoryIcon = null + sortOrderMenu = null + layout = null categoriesList = null viewPager = null @@ -316,6 +286,7 @@ class TabsFragment: Fragment() { outState.putByte(STATE_SHOW_CATEGORIES, if (showCategories) 1 else 0) outState.putStringArrayList(STATE_CATEGORIES, ArrayList(categories)) outState.putString(STATE_CATEGORY, category) + outState.putString(STATE_ORDER, order.name) } override fun onViewStateRestored(savedInstanceState: Bundle?) { @@ -333,6 +304,7 @@ class TabsFragment: Fragment() { if (view != null && childFragment is ProductsFragment) { childFragment.setSearchQuery(searchQuery) childFragment.setCategory(category) + childFragment.setOrder(order) } } @@ -340,7 +312,7 @@ class TabsFragment: Fragment() { if (view == null) { needSelectUpdates = true } else { - tabLayout?.getTabAt(ProductsFragment.Source.UPDATES.ordinal)!!.select() + layout?.tabLayout?.getTabAt(ProductsFragment.Source.UPDATES.ordinal)!!.select() } } @@ -354,20 +326,12 @@ class TabsFragment: Fragment() { } private fun updateCategory() { - val categories = categories - val category = category - val categoryName = categoryName!! - val index = categories.indexOf(category) - if (index < 0) { - categoryName.setText(R.string.all_applications_category) - if (category.isNotEmpty()) { - this.category = "" - } - } else { - categoryName.text = category + if (category.isNotEmpty() && categories.indexOf(category) < 0) { + category = "" } - categoryIcon?.visibility = if (categories.isEmpty()) View.GONE else View.VISIBLE - productFragments.forEach { it.setCategory(this.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() } @@ -404,4 +368,86 @@ class TabsFragment: Fragment() { } } } + + private val pageChangeListener = object: ViewPager.OnPageChangeListener { + 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 + } else { + if (fromCategories) 1f else 0f + } + if (layout.categoryLayout.childCount != 1) { + throw RuntimeException() + } + val child = layout.categoryLayout.getChildAt(0) + val height = child.layoutParams.height + if (height <= 0) { + throw RuntimeException() + } + val currentHeight = (offset * height).roundToInt() + if (layout.categoryLayout.layoutParams.height != currentHeight) { + layout.categoryLayout.layoutParams.height = currentHeight + layout.categoryLayout.requestLayout() + } + } + + override fun onPageSelected(position: Int) { + val layout = layout!! + val source = ProductsFragment.Source.values()[position] + updateUpdateNotificationBlocker(source) + sortOrderMenu!!.first.isVisible = source.order + layout.tabLayout.selectTab(layout.tabLayout.getTabAt(source.ordinal)) + if (showCategories && !source.categories) { + showCategories = false + } + } + + override fun onPageScrollStateChanged(state: Int) { + layout!!.categoryChange.isEnabled = state != ViewPager.SCROLL_STATE_DRAGGING && + ProductsFragment.Source.values()[viewPager!!.currentItem].categories + } + } + + private class CategoriesAdapter(private val categories: () -> List, private val onClick: (String) -> Unit): + EnumRecyclerAdapter() { + enum class ViewType { CATEGORY } + + private class CategoryViewHolder(context: Context): RecyclerView.ViewHolder(AppCompatTextView(context)) { + val title: TextView + get() = itemView as TextView + + init { + itemView as TextView + itemView.gravity = Gravity.CENTER_VERTICAL + itemView.resources.sizeScaled(16).let { itemView.setPadding(it, 0, it, 0) } + itemView.setTextColor(context.getColorFromAttr(android.R.attr.textColorPrimary)) + itemView.setTextSizeScaled(16) + itemView.background = context.getDrawableFromAttr(R.attr.selectableItemBackground) + itemView.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, + itemView.resources.sizeScaled(48)) + } + } + + 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 onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder { + return CategoryViewHolder(parent.context).apply { + itemView.setOnClickListener { onClick(categories().getOrNull(adapterPosition - 1).orEmpty()) } + } + } + + 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) + } + } } diff --git a/src/main/kotlin/nya/kitsunyan/foxydroid/service/SyncService.kt b/src/main/kotlin/nya/kitsunyan/foxydroid/service/SyncService.kt index 7853ead..cbadf20 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: Service() { if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) { val disposable = RxUtils .querySingle { Database.ProductAdapter - .query(true, true, "", "", it) + .query(true, true, "", "", ProductItem.Order.NAME, it) .use { it.asSequence().map(Database.ProductAdapter::transformItem).toList() } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/src/main/res/drawable/ic_sort.xml b/src/main/res/drawable/ic_sort.xml new file mode 100644 index 0000000..d016b7b --- /dev/null +++ b/src/main/res/drawable/ic_sort.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/src/main/res/layout/fragment.xml b/src/main/res/layout/fragment.xml index 7199e92..441c2b6 100644 --- a/src/main/res/layout/fragment.xml +++ b/src/main/res/layout/fragment.xml @@ -1,6 +1,7 @@ @@ -13,7 +14,8 @@ + android:layout_height="wrap_content" + app:popupTheme="@style/Theme.Toolbar.Popup" /> Action failed Add repository Address - All Applications + All applications Already exists Always Has advertising @@ -35,6 +35,7 @@ Confirm action Connecting Dark + Date added Delete Are you sure you want to delete the repository? Description @@ -124,6 +125,7 @@ Show older releases Skip SOCKS proxy + Sort order Source code Suggested Sync repositories diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml index 85fa923..e0e8eb0 100644 --- a/src/main/res/values/styles.xml +++ b/src/main/res/values/styles.xml @@ -23,4 +23,8 @@ @color/error_dark + +