Allow to sort by date added and last update

This commit is contained in:
kitsunyan
2020-06-16 12:01:53 +03:00
parent bb50d77547
commit 561352b685
11 changed files with 232 additions and 124 deletions
@@ -5,22 +5,26 @@ import android.os.Bundle
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.loader.app.LoaderManager import androidx.loader.app.LoaderManager
import androidx.loader.content.Loader import androidx.loader.content.Loader
import nya.kitsunyan.foxydroid.entity.ProductItem
class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> { class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
sealed class Request { sealed class Request {
internal abstract val id: Int 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 override val id: Int
get() = 1 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 override val id: Int
get() = 2 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 override val id: Int
get() = 3 get() = 3
} }
@@ -75,11 +79,11 @@ class CursorOwner: Fragment(), LoaderManager.LoaderCallbacks<Cursor> {
return QueryLoader(requireContext()) { return QueryLoader(requireContext()) {
when (request) { when (request) {
is Request.ProductsAvailable -> Database.ProductAdapter 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 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 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) is Request.Repositories -> Database.RepositoryAdapter.query(it)
} }
} }
@@ -75,6 +75,8 @@ object Database {
const val ROW_PACKAGE_NAME = "package_name" const val ROW_PACKAGE_NAME = "package_name"
const val ROW_NAME = "name" const val ROW_NAME = "name"
const val ROW_SUMMARY = "summary" 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_VERSION_CODE = "version_code"
const val ROW_SIGNATURE = "signature" const val ROW_SIGNATURE = "signature"
const val ROW_COMPATIBLE = "compatible" const val ROW_COMPATIBLE = "compatible"
@@ -88,6 +90,8 @@ object Database {
$ROW_PACKAGE_NAME TEXT NOT NULL, $ROW_PACKAGE_NAME TEXT NOT NULL,
$ROW_NAME TEXT NOT NULL, $ROW_NAME TEXT NOT NULL,
$ROW_SUMMARY 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_VERSION_CODE INTEGER NOT NULL,
$ROW_SIGNATURE TEXT NOT NULL, $ROW_SIGNATURE TEXT NOT NULL,
$ROW_COMPATIBLE INTEGER NOT NULL, $ROW_COMPATIBLE INTEGER NOT NULL,
@@ -386,7 +390,7 @@ object Database {
} }
fun query(installed: Boolean, updates: Boolean, searchQuery: String, fun query(installed: Boolean, updates: Boolean, searchQuery: String,
category: String, signal: CancellationSignal?): Cursor { category: String, order: ProductItem.Order, signal: CancellationSignal?): Cursor {
val builder = QueryBuilder() val builder = QueryBuilder()
builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID}, builder += """SELECT product.rowid AS _id, product.${Schema.Product.ROW_REPOSITORY_ID},
@@ -432,7 +436,13 @@ object Database {
if (updates) { if (updates) {
builder += "AND ${Schema.Synthetic.ROW_CAN_UPDATE}" 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) 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_PACKAGE_NAME, product.packageName)
put(Schema.Product.ROW_NAME, product.name) put(Schema.Product.ROW_NAME, product.name)
put(Schema.Product.ROW_SUMMARY, product.summary) 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_VERSION_CODE, product.versionCode)
put(Schema.Product.ROW_SIGNATURE, product.signature) put(Schema.Product.ROW_SIGNATURE, product.signature)
put(Schema.Product.ROW_COMPATIBLE, if (product.compatible) 1 else 0) put(Schema.Product.ROW_COMPATIBLE, if (product.compatible) 1 else 0)
@@ -2,11 +2,18 @@ package nya.kitsunyan.foxydroid.entity
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import nya.kitsunyan.foxydroid.R
import nya.kitsunyan.foxydroid.utility.extension.json.* import nya.kitsunyan.foxydroid.utility.extension.json.*
data class ProductItem(val repositoryId: Long, val packageName: String, 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 name: String, val summary: String, val icon: String, val version: String, val installedVersion: String,
val compatible: Boolean, val canUpdate: Boolean) { 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) { fun serialize(generator: JsonGenerator) {
generator.writeNumberField("serialVersion", 1) generator.writeNumberField("serialVersion", 1)
generator.writeStringField("icon", icon) generator.writeStringField("icon", icon)
@@ -16,6 +16,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import nya.kitsunyan.foxydroid.R import nya.kitsunyan.foxydroid.R
import nya.kitsunyan.foxydroid.database.CursorOwner import nya.kitsunyan.foxydroid.database.CursorOwner
import nya.kitsunyan.foxydroid.database.Database import nya.kitsunyan.foxydroid.database.Database
import nya.kitsunyan.foxydroid.entity.ProductItem
import nya.kitsunyan.foxydroid.utility.RxUtils import nya.kitsunyan.foxydroid.utility.RxUtils
import nya.kitsunyan.foxydroid.utility.extension.resources.* import nya.kitsunyan.foxydroid.utility.extension.resources.*
import nya.kitsunyan.foxydroid.widget.DividerItemDecoration 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_SEARCH_QUERY = "currentSearchQuery"
private const val STATE_CURRENT_CATEGORY = "currentCategory" private const val STATE_CURRENT_CATEGORY = "currentCategory"
private const val STATE_CURRENT_ORDER = "currentOrder"
private const val STATE_LAYOUT_MANAGER = "layoutManager" private const val STATE_LAYOUT_MANAGER = "layoutManager"
} }
enum class Source(val titleResId: Int, val categories: Boolean) { enum class Source(val titleResId: Int, val categories: Boolean, val order: Boolean) {
AVAILABLE(R.string.available, true), AVAILABLE(R.string.available, true, true),
INSTALLED(R.string.installed, false), INSTALLED(R.string.installed, false, false),
UPDATES(R.string.updates, false) UPDATES(R.string.updates, false, false)
} }
constructor(source: Source): this() { constructor(source: Source): this() {
@@ -47,9 +49,11 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback {
private var searchQuery = "" private var searchQuery = ""
private var category = "" private var category = ""
private var order = ProductItem.Order.NAME
private var currentSearchQuery = "" private var currentSearchQuery = ""
private var currentCategory = "" private var currentCategory = ""
private var currentOrder = ProductItem.Order.NAME
private var layoutManagerState: Parcelable? = null private var layoutManagerState: Parcelable? = null
private var recyclerView: RecyclerView? = null private var recyclerView: RecyclerView? = null
@@ -60,10 +64,11 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback {
get() { get() {
val searchQuery = searchQuery val searchQuery = searchQuery
val category = if (source.categories) category else "" val category = if (source.categories) category else ""
val order = if (source.order) order else ProductItem.Order.NAME
return when (source) { return when (source) {
Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, category) Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, category, order)
Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, category) Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, category, order)
Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, category) 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() currentSearchQuery = savedInstanceState?.getString(STATE_CURRENT_SEARCH_QUERY).orEmpty()
currentCategory = savedInstanceState?.getString(STATE_CURRENT_CATEGORY).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) layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
screenActivity.cursorOwner.attach(this, request) 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_SEARCH_QUERY, currentSearchQuery)
outState.putString(STATE_CURRENT_CATEGORY, currentCategory) outState.putString(STATE_CURRENT_CATEGORY, currentCategory)
outState.putString(STATE_CURRENT_ORDER, currentOrder.name)
(layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState()) (layoutManagerState ?: recyclerView?.layoutManager?.onSaveInstanceState())
?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) } ?.let { outState.putParcelable(STATE_LAYOUT_MANAGER, it) }
} }
@@ -137,9 +145,10 @@ class ProductsFragment(): Fragment(), CursorOwner.Callback {
recyclerView?.layoutManager?.onRestoreInstanceState(it) recyclerView?.layoutManager?.onRestoreInstanceState(it)
} }
if (currentSearchQuery != searchQuery || currentCategory != category) { if (currentSearchQuery != searchQuery || currentCategory != category || currentOrder != order) {
currentSearchQuery = searchQuery currentSearchQuery = searchQuery
currentCategory = category currentCategory = category
currentOrder = order
recyclerView?.scrollToPosition(0) 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)
}
}
}
} }
@@ -124,7 +124,7 @@ class RepositoryFragment(): Fragment() {
getString(R.string.unknown) 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, layout.addTitleText(R.string.number_of_applications,
Database.ProductAdapter.getCount(repository.id).toString()) Database.ProductAdapter.getCount(repository.id).toString())
} }
@@ -1,6 +1,7 @@
package nya.kitsunyan.foxydroid.screen package nya.kitsunyan.foxydroid.screen
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -15,6 +16,7 @@ import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView import androidx.appcompat.widget.AppCompatTextView
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.MenuCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentStatePagerAdapter import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@@ -27,11 +29,14 @@ import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import nya.kitsunyan.foxydroid.R import nya.kitsunyan.foxydroid.R
import nya.kitsunyan.foxydroid.database.Database import nya.kitsunyan.foxydroid.database.Database
import nya.kitsunyan.foxydroid.entity.ProductItem
import nya.kitsunyan.foxydroid.service.Connection import nya.kitsunyan.foxydroid.service.Connection
import nya.kitsunyan.foxydroid.service.SyncService import nya.kitsunyan.foxydroid.service.SyncService
import nya.kitsunyan.foxydroid.utility.RxUtils import nya.kitsunyan.foxydroid.utility.RxUtils
import nya.kitsunyan.foxydroid.utility.Utils import nya.kitsunyan.foxydroid.utility.Utils
import nya.kitsunyan.foxydroid.utility.extension.resources.* import nya.kitsunyan.foxydroid.utility.extension.resources.*
import nya.kitsunyan.foxydroid.utility.extension.text.*
import nya.kitsunyan.foxydroid.widget.EnumRecyclerAdapter
import kotlin.math.* import kotlin.math.*
class TabsFragment: Fragment() { class TabsFragment: Fragment() {
@@ -40,11 +45,19 @@ class TabsFragment: Fragment() {
private const val STATE_SHOW_CATEGORIES = "showCategories" private const val STATE_SHOW_CATEGORIES = "showCategories"
private const val STATE_CATEGORIES = "categories" private const val STATE_CATEGORIES = "categories"
private const val STATE_CATEGORY = "category" private const val STATE_CATEGORY = "category"
private const val STATE_ORDER = "order"
} }
private var tabLayout: TabLayout? = null private class Layout(view: View) {
private var categoryName: TextView? = null val tabLayout = view.findViewById<TabLayout>(R.id.tabs)!!
private var categoryIcon: ImageView? = null val categoryLayout = view.findViewById<ViewGroup>(R.id.category_layout)!!
val categoryChange = view.findViewById<View>(R.id.category_change)!!
val categoryName = view.findViewById<TextView>(R.id.category_name)!!
val categoryIcon = view.findViewById<ImageView>(R.id.category_icon)!!
}
private var sortOrderMenu: Pair<MenuItem, List<MenuItem>>? = null
private var layout: Layout? = null
private var categoriesList: RecyclerView? = null private var categoriesList: RecyclerView? = null
private var viewPager: ViewPager? = null private var viewPager: ViewPager? = null
@@ -52,9 +65,10 @@ class TabsFragment: Fragment() {
set(value) { set(value) {
if (field != value) { if (field != value) {
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 } } .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) { if ((categoriesList?.parent as? View)?.height ?: 0 > 0) {
animateCategoriesList() animateCategoriesList()
} }
@@ -64,6 +78,7 @@ class TabsFragment: Fragment() {
private var searchQuery = "" private var searchQuery = ""
private var categories = emptyList<String>() private var categories = emptyList<String>()
private var category = "" private var category = ""
private var order = ProductItem.Order.NAME
private val syncConnection = Connection<SyncService.Binder>(SyncService::class.java, onBind = { private val syncConnection = Connection<SyncService.Binder>(SyncService::class.java, onBind = {
viewPager?.let { viewPager?.let {
@@ -112,26 +127,44 @@ class TabsFragment: Fragment() {
}) })
toolbar.menu.apply { toolbar.menu.apply {
MenuCompat.setGroupDividerEnabled(this, true)
add(0, R.id.toolbar_search, 0, R.string.search) add(0, R.id.toolbar_search, 0, R.string.search)
.setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_search)) .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_search))
.setActionView(searchView) .setActionView(searchView)
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS or MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW) .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)) .setIcon(Utils.getToolbarIcon(toolbar.context, R.drawable.ic_sync))
.setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_ALWAYS) .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM)
.setOnMenuItemClickListener { .setOnMenuItemClickListener {
syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL) syncConnection.binder?.sync(SyncService.SyncRequest.MANUAL)
true true
} }
add(R.string.repositories) add(1, 0, 0, R.string.repositories)
.setOnMenuItemClickListener { .setOnMenuItemClickListener {
view.post { screenActivity.navigateRepositories() } view.post { screenActivity.navigateRepositories() }
true true
} }
add(R.string.preferences) add(1, 0, 0, R.string.preferences)
.setOnMenuItemClickListener { .setOnMenuItemClickListener {
view.post { screenActivity.navigatePreferences() } view.post { screenActivity.navigatePreferences() }
true true
@@ -143,11 +176,12 @@ class TabsFragment: Fragment() {
val toolbarExtra = view.findViewById<FrameLayout>(R.id.toolbar_extra) val toolbarExtra = view.findViewById<FrameLayout>(R.id.toolbar_extra)
toolbarExtra.addView(toolbarExtra.inflate(R.layout.tabs_toolbar)) toolbarExtra.addView(toolbarExtra.inflate(R.layout.tabs_toolbar))
val layout = Layout(view)
this.layout = layout
val tabLayout = view.findViewById<TabLayout>(R.id.tabs) ProductsFragment.Source.values().forEach { layout.tabLayout
this.tabLayout = tabLayout .addTab(layout.tabLayout.newTab().setText(it.titleResId)) }
ProductsFragment.Source.values().forEach { tabLayout.addTab(tabLayout.newTab().setText(it.titleResId)) } layout.tabLayout.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener {
tabLayout.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) { override fun onTabSelected(tab: TabLayout.Tab) {
viewPager!!.currentItem = tab.position viewPager!!.currentItem = tab.position
} }
@@ -156,16 +190,14 @@ class TabsFragment: Fragment() {
override fun onTabReselected(tab: TabLayout.Tab) = Unit override fun onTabReselected(tab: TabLayout.Tab) = Unit
}) })
val categoryLayout = view.findViewById<ViewGroup>(R.id.category_layout)
val categoryChange = view.findViewById<View>(R.id.category_change)
val categoryName = view.findViewById<TextView>(R.id.category_name)
val categoryIcon = view.findViewById<ImageView>(R.id.category_icon)
this.categoryName = categoryName
this.categoryIcon = categoryIcon
showCategories = savedInstanceState?.getByte(STATE_SHOW_CATEGORIES)?.toInt() ?: 0 != 0 showCategories = savedInstanceState?.getByte(STATE_SHOW_CATEGORIES)?.toInt() ?: 0 != 0
categories = savedInstanceState?.getStringArrayList(STATE_CATEGORIES).orEmpty() categories = savedInstanceState?.getStringArrayList(STATE_CATEGORIES).orEmpty()
category = savedInstanceState?.getString(STATE_CATEGORY).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<FrameLayout>(R.id.fragment_content) val content = view.findViewById<FrameLayout>(R.id.fragment_content)
@@ -176,45 +208,7 @@ class TabsFragment: Fragment() {
override fun getCount(): Int = ProductsFragment.Source.values().size override fun getCount(): Int = ProductsFragment.Source.values().size
} }
content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
addOnPageChangeListener(object: ViewPager.OnPageChangeListener { addOnPageChangeListener(pageChangeListener)
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
}
})
} }
categoriesDisposable = Observable.just(Unit) categoriesDisposable = Observable.just(Unit)
@@ -237,36 +231,13 @@ class TabsFragment: Fragment() {
isMotionEventSplittingEnabled = false isMotionEventSplittingEnabled = false
isVerticalScrollBarEnabled = false isVerticalScrollBarEnabled = false
setHasFixedSize(true) setHasFixedSize(true)
adapter = object: RecyclerView.Adapter<RecyclerView.ViewHolder>() { this.adapter = CategoriesAdapter({ categories }) {
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) { if (showCategories) {
showCategories = false showCategories = false
category = if (adapterPosition == 0) "" else categories[adapterPosition - 1] category = it
updateCategory() 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]
}
}
setBackgroundColor(context.getColorFromAttr(R.attr.colorPrimaryDark).defaultColor) setBackgroundColor(context.getColorFromAttr(R.attr.colorPrimaryDark).defaultColor)
elevation = resources.sizeScaled(4).toFloat() elevation = resources.sizeScaled(4).toFloat()
content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, 0) content.addView(this, FrameLayout.LayoutParams.MATCH_PARENT, 0)
@@ -296,9 +267,8 @@ class TabsFragment: Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
tabLayout = null sortOrderMenu = null
categoryName = null layout = null
categoryIcon = null
categoriesList = null categoriesList = null
viewPager = null viewPager = null
@@ -316,6 +286,7 @@ class TabsFragment: Fragment() {
outState.putByte(STATE_SHOW_CATEGORIES, if (showCategories) 1 else 0) outState.putByte(STATE_SHOW_CATEGORIES, if (showCategories) 1 else 0)
outState.putStringArrayList(STATE_CATEGORIES, ArrayList(categories)) outState.putStringArrayList(STATE_CATEGORIES, ArrayList(categories))
outState.putString(STATE_CATEGORY, category) outState.putString(STATE_CATEGORY, category)
outState.putString(STATE_ORDER, order.name)
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun onViewStateRestored(savedInstanceState: Bundle?) {
@@ -333,6 +304,7 @@ class TabsFragment: Fragment() {
if (view != null && childFragment is ProductsFragment) { if (view != null && childFragment is ProductsFragment) {
childFragment.setSearchQuery(searchQuery) childFragment.setSearchQuery(searchQuery)
childFragment.setCategory(category) childFragment.setCategory(category)
childFragment.setOrder(order)
} }
} }
@@ -340,7 +312,7 @@ class TabsFragment: Fragment() {
if (view == null) { if (view == null) {
needSelectUpdates = true needSelectUpdates = true
} else { } 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() { private fun updateCategory() {
val categories = categories if (category.isNotEmpty() && categories.indexOf(category) < 0) {
val category = 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 { layout?.categoryName?.text = category.nullIfEmpty() ?: getString(R.string.all_applications)
categoryName.text = category layout?.categoryIcon?.visibility = if (categories.isEmpty()) View.GONE else View.VISIBLE
} productFragments.forEach { it.setCategory(category) }
categoryIcon?.visibility = if (categories.isEmpty()) View.GONE else View.VISIBLE
productFragments.forEach { it.setCategory(this.category) }
categoriesList?.adapter?.notifyDataSetChanged() 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<String>, private val onClick: (String) -> Unit):
EnumRecyclerAdapter<CategoriesAdapter.ViewType, RecyclerView.ViewHolder>() {
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<ViewType>
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)
}
}
} }
@@ -316,7 +316,7 @@ class SyncService: Service() {
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) { if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
val disposable = RxUtils val disposable = RxUtils
.querySingle { Database.ProductAdapter .querySingle { Database.ProductAdapter
.query(true, true, "", "", it) .query(true, true, "", "", ProductItem.Order.NAME, it)
.use { it.asSequence().map(Database.ProductAdapter::transformItem).toList() } } .use { it.asSequence().map(Database.ProductAdapter::transformItem).toList() } }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffffff"
android:pathData="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z" />
</vector>
+3 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<nya.kitsunyan.foxydroid.widget.FragmentLinearLayout <nya.kitsunyan.foxydroid.widget.FragmentLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
@@ -13,7 +14,8 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" /> android:layout_height="wrap_content"
app:popupTheme="@style/Theme.Toolbar.Popup" />
<FrameLayout <FrameLayout
android:id="@+id/toolbar_extra" android:id="@+id/toolbar_extra"
+3 -1
View File
@@ -4,7 +4,7 @@
<string name="action_failed">Action failed</string> <string name="action_failed">Action failed</string>
<string name="add_repository">Add repository</string> <string name="add_repository">Add repository</string>
<string name="address">Address</string> <string name="address">Address</string>
<string name="all_applications_category">All Applications</string> <string name="all_applications">All applications</string>
<string name="already_exists">Already exists</string> <string name="already_exists">Already exists</string>
<string name="always">Always</string> <string name="always">Always</string>
<string name="anti_feature_advertising">Has advertising</string> <string name="anti_feature_advertising">Has advertising</string>
@@ -35,6 +35,7 @@
<string name="confirm_action">Confirm action</string> <string name="confirm_action">Confirm action</string>
<string name="connecting">Connecting</string> <string name="connecting">Connecting</string>
<string name="dark">Dark</string> <string name="dark">Dark</string>
<string name="date_added">Date added</string>
<string name="delete">Delete</string> <string name="delete">Delete</string>
<string name="delete_repository_confirm">Are you sure you want to delete the repository?</string> <string name="delete_repository_confirm">Are you sure you want to delete the repository?</string>
<string name="description">Description</string> <string name="description">Description</string>
@@ -124,6 +125,7 @@
<string name="show_older_releases">Show older releases</string> <string name="show_older_releases">Show older releases</string>
<string name="skip">Skip</string> <string name="skip">Skip</string>
<string name="socks_proxy">SOCKS proxy</string> <string name="socks_proxy">SOCKS proxy</string>
<string name="sort_order">Sort order</string>
<string name="source_code">Source code</string> <string name="source_code">Source code</string>
<string name="suggested">Suggested</string> <string name="suggested">Suggested</string>
<string name="sync_repositories">Sync repositories</string> <string name="sync_repositories">Sync repositories</string>
+4
View File
@@ -23,4 +23,8 @@
<item name="colorError">@color/error_dark</item> <item name="colorError">@color/error_dark</item>
</style> </style>
<style name="Theme.Toolbar.Popup" parent="Theme.Main.Dark">
<item name="android:colorBackground">?attr/colorPrimaryDark</item>
</style>
</resources> </resources>