Allow to view packages per repository

This commit is contained in:
kitsunyan
2020-07-30 07:03:32 +03:00
parent 6467b23c65
commit 09067cd2d4
8 changed files with 219 additions and 123 deletions
@@ -11,19 +11,19 @@ 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, data class ProductsAvailable(val searchQuery: String, val section: ProductItem.Section,
val order: ProductItem.Order): Request() { 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, data class ProductsInstalled(val searchQuery: String, val section: ProductItem.Section,
val order: ProductItem.Order): Request() { 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, data class ProductsUpdates(val searchQuery: String, val section: ProductItem.Section,
val order: ProductItem.Order): Request() { val order: ProductItem.Order): Request() {
override val id: Int override val id: Int
get() = 3 get() = 3
@@ -79,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, request.order, it) .query(false, false, request.searchQuery, request.section, request.order, it)
is Request.ProductsInstalled -> Database.ProductAdapter 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 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) is Request.Repositories -> Database.RepositoryAdapter.query(it)
} }
} }
@@ -395,7 +395,7 @@ object Database {
} }
fun query(installed: Boolean, updates: Boolean, searchQuery: String, 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 builder = QueryBuilder()
val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND val signatureMatches = """installed.${Schema.Installed.ROW_SIGNATURE} IS NOT NULL AND
@@ -434,16 +434,19 @@ object Database {
} }
builder += """JOIN ${Schema.Installed.name} AS installed builder += """JOIN ${Schema.Installed.name} AS installed
ON product.${Schema.Product.ROW_PACKAGE_NAME} = installed.${Schema.Installed.ROW_PACKAGE_NAME}""" 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 builder += """JOIN ${Schema.Category.name} AS category
ON product.${Schema.Product.ROW_PACKAGE_NAME} = category.${Schema.Product.ROW_PACKAGE_NAME}""" ON product.${Schema.Product.ROW_PACKAGE_NAME} = category.${Schema.Product.ROW_PACKAGE_NAME}"""
} }
builder += """WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND builder += """WHERE repository.${Schema.Repository.ROW_ENABLED} != 0 AND
repository.${Schema.Repository.ROW_DELETED} == 0""" repository.${Schema.Repository.ROW_DELETED} == 0"""
if (category.isNotEmpty()) { if (section is ProductItem.Section.Category) {
builder += "AND category.${Schema.Category.ROW_NAME} = ?" 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()) { if (searchQuery.isNotEmpty()) {
builder += """AND ${Schema.Synthetic.ROW_MATCH_RANK} > 0""" builder += """AND ${Schema.Synthetic.ROW_MATCH_RANK} > 0"""
@@ -1,13 +1,49 @@
package nya.kitsunyan.foxydroid.entity package nya.kitsunyan.foxydroid.entity
import android.os.Parcel
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.R
import nya.kitsunyan.foxydroid.utility.KParcelable
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 matchRank: Int) { 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) { enum class Order(val titleResId: Int) {
NAME(R.string.name), NAME(R.string.name),
DATE_ADDED(R.string.date_added), DATE_ADDED(R.string.date_added),
@@ -25,12 +25,12 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
private const val EXTRA_SOURCE = "source" private const val EXTRA_SOURCE = "source"
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_SECTION = "currentSection"
private const val STATE_CURRENT_ORDER = "currentOrder" 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, val order: Boolean) { enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean) {
AVAILABLE(R.string.available, true, true), AVAILABLE(R.string.available, true, true),
INSTALLED(R.string.installed, false, false), INSTALLED(R.string.installed, false, false),
UPDATES(R.string.updates, 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) get() = requireArguments().getString(EXTRA_SOURCE)!!.let(Source::valueOf)
private var searchQuery = "" private var searchQuery = ""
private var category = "" private var section: ProductItem.Section = ProductItem.Section.All
private var order = ProductItem.Order.NAME private var order = ProductItem.Order.NAME
private var currentSearchQuery = "" private var currentSearchQuery = ""
private var currentCategory = "" private var currentSection: ProductItem.Section = ProductItem.Section.All
private var currentOrder = ProductItem.Order.NAME private var currentOrder = ProductItem.Order.NAME
private var layoutManagerState: Parcelable? = null private var layoutManagerState: Parcelable? = null
@@ -61,12 +61,12 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
private val request: CursorOwner.Request private val request: CursorOwner.Request
get() { get() {
val searchQuery = searchQuery 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 val order = if (source.order) order else ProductItem.Order.NAME
return when (source) { return when (source) {
Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, category, order) Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable(searchQuery, section, order)
Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, category, order) Source.INSTALLED -> CursorOwner.Request.ProductsInstalled(searchQuery, section, order)
Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, category, order) Source.UPDATES -> CursorOwner.Request.ProductsUpdates(searchQuery, section, order)
} }
} }
@@ -90,7 +90,7 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
currentSearchQuery = savedInstanceState?.getString(STATE_CURRENT_SEARCH_QUERY).orEmpty() 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) currentOrder = savedInstanceState?.getString(STATE_CURRENT_ORDER)
?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME ?.let(ProductItem.Order::valueOf) ?: ProductItem.Order.NAME
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
@@ -119,7 +119,7 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putString(STATE_CURRENT_SEARCH_QUERY, currentSearchQuery) 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) 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) }
@@ -144,9 +144,9 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
recyclerView?.layoutManager?.onRestoreInstanceState(it) recyclerView?.layoutManager?.onRestoreInstanceState(it)
} }
if (currentSearchQuery != searchQuery || currentCategory != category || currentOrder != order) { if (currentSearchQuery != searchQuery || currentSection != section || currentOrder != order) {
currentSearchQuery = searchQuery currentSearchQuery = searchQuery
currentCategory = category currentSection = section
currentOrder = order currentOrder = order
recyclerView?.scrollToPosition(0) recyclerView?.scrollToPosition(0)
} }
@@ -161,9 +161,9 @@ class ProductsFragment(): ScreenFragment(), CursorOwner.Callback {
} }
} }
internal fun setCategory(category: String) { internal fun setSection(section: ProductItem.Section) {
if (this.category != category) { if (this.section != section) {
this.category = category this.section = section
if (view != null) { if (view != null) {
screenActivity.cursorOwner.attach(this, request) screenActivity.cursorOwner.attach(this, request)
} }
@@ -34,13 +34,14 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import nya.kitsunyan.foxydroid.R import nya.kitsunyan.foxydroid.R
import nya.kitsunyan.foxydroid.content.Preferences import nya.kitsunyan.foxydroid.content.Preferences
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.android.* import nya.kitsunyan.foxydroid.utility.extension.android.*
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.DividerItemDecoration
import nya.kitsunyan.foxydroid.widget.EnumRecyclerAdapter import nya.kitsunyan.foxydroid.widget.EnumRecyclerAdapter
import nya.kitsunyan.foxydroid.widget.FocusSearchView import nya.kitsunyan.foxydroid.widget.FocusSearchView
import kotlin.math.* import kotlin.math.*
@@ -49,43 +50,43 @@ class TabsFragment: ScreenFragment() {
companion object { companion object {
private const val STATE_SEARCH_FOCUSED = "searchFocused" private const val STATE_SEARCH_FOCUSED = "searchFocused"
private const val STATE_SEARCH_QUERY = "searchQuery" private const val STATE_SEARCH_QUERY = "searchQuery"
private const val STATE_SHOW_CATEGORIES = "showCategories" private const val STATE_SHOW_SECTIONS = "showSections"
private const val STATE_CATEGORIES = "categories" private const val STATE_SECTIONS = "sections"
private const val STATE_CATEGORY = "category" private const val STATE_SECTION = "section"
} }
private class Layout(view: View) { private class Layout(view: View) {
val tabs = view.findViewById<LinearLayout>(R.id.tabs)!! val tabs = view.findViewById<LinearLayout>(R.id.tabs)!!
val categoryLayout = view.findViewById<ViewGroup>(R.id.category_layout)!! val sectionLayout = view.findViewById<ViewGroup>(R.id.section_layout)!!
val categoryChange = view.findViewById<View>(R.id.category_change)!! val sectionChange = view.findViewById<View>(R.id.section_change)!!
val categoryName = view.findViewById<TextView>(R.id.category_name)!! val sectionName = view.findViewById<TextView>(R.id.section_name)!!
val categoryIcon = view.findViewById<ImageView>(R.id.category_icon)!! val sectionIcon = view.findViewById<ImageView>(R.id.section_icon)!!
} }
private var searchMenuItem: MenuItem? = null private var searchMenuItem: MenuItem? = null
private var sortOrderMenu: Pair<MenuItem, List<MenuItem>>? = null private var sortOrderMenu: Pair<MenuItem, List<MenuItem>>? = null
private var syncRepositoriesMenuItem: MenuItem? = null private var syncRepositoriesMenuItem: MenuItem? = null
private var layout: Layout? = null private var layout: Layout? = null
private var categoriesList: RecyclerView? = null private var sectionsList: RecyclerView? = null
private var viewPager: ViewPager2? = null private var viewPager: ViewPager2? = null
private var showCategories = false private var showSections = false
set(value) { set(value) {
if (field != value) { if (field != value) {
field = value field = value
val layout = layout val layout = layout
layout?.tabs?.let { (0 until it.childCount) layout?.tabs?.let { (0 until it.childCount)
.forEach { index -> it.getChildAt(index)!!.isEnabled = !value } } .forEach { index -> it.getChildAt(index)!!.isEnabled = !value } }
layout?.categoryIcon?.scaleY = if (value) -1f else 1f layout?.sectionIcon?.scaleY = if (value) -1f else 1f
if ((categoriesList?.parent as? View)?.height ?: 0 > 0) { if ((sectionsList?.parent as? View)?.height ?: 0 > 0) {
animateCategoriesList() animateSectionsList()
} }
} }
} }
private var searchQuery = "" private var searchQuery = ""
private var categories = emptyList<String>() private var sections = listOf<ProductItem.Section>(ProductItem.Section.All)
private var category = "" private var section: ProductItem.Section = ProductItem.Section.All
private val syncConnection = Connection(SyncService::class.java, onBind = { _, _ -> private val syncConnection = Connection(SyncService::class.java, onBind = { _, _ ->
viewPager?.let { viewPager?.let {
@@ -96,7 +97,8 @@ class TabsFragment: ScreenFragment() {
private var sortOrderDisposable: Disposable? = null private var sortOrderDisposable: Disposable? = null
private var categoriesDisposable: 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 private var needSelectUpdates = false
@@ -212,10 +214,11 @@ class TabsFragment: ScreenFragment() {
(tab.layoutParams as LinearLayout.LayoutParams).weight = 1f (tab.layoutParams as LinearLayout.LayoutParams).weight = 1f
} }
showCategories = savedInstanceState?.getByte(STATE_SHOW_CATEGORIES)?.toInt() ?: 0 != 0 showSections = savedInstanceState?.getByte(STATE_SHOW_SECTIONS)?.toInt() ?: 0 != 0
categories = savedInstanceState?.getStringArrayList(STATE_CATEGORIES).orEmpty() sections = savedInstanceState?.getParcelableArrayList<ProductItem.Section>(STATE_SECTIONS).orEmpty()
category = savedInstanceState?.getString(STATE_CATEGORY).orEmpty() section = savedInstanceState?.getParcelable(STATE_SECTION) ?: ProductItem.Section.All
layout.categoryChange.setOnClickListener { showCategories = categories.isNotEmpty() && !showCategories } layout.sectionChange.setOnClickListener { showSections = sections
.any { it !is ProductItem.Section.All } && !showSections }
updateOrder() updateOrder()
sortOrderDisposable = Preferences.observable.subscribe { sortOrderDisposable = Preferences.observable.subscribe {
@@ -243,34 +246,38 @@ class TabsFragment: ScreenFragment() {
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } } .flatMapSingle { RxUtils.querySingle { Database.CategoryAdapter.getAll(it) } }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { .subscribe { setSectionsAndUpdate(it.asSequence().sorted()
val categories = it.sorted() .map(ProductItem.Section::Category).toList(), null) }
if (this.categories != categories) { repositoriesDisposable = Observable.just(Unit)
this.categories = categories .concatWith(Database.observable(Database.Subject.Repositories))
updateCategory() .observeOn(Schedulers.io())
} .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } }
} .observeOn(AndroidSchedulers.mainThread())
updateCategory() .subscribe { setSectionsAndUpdate(null, it.asSequence().filter { it.enabled }
.map { ProductItem.Section.Repository(it.id, it.name) }.toList()) }
updateSection()
val categoriesList = RecyclerView(toolbar.context).apply { val sectionsList = RecyclerView(toolbar.context).apply {
id = R.id.categories_list id = R.id.sections_list
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
isMotionEventSplittingEnabled = false isMotionEventSplittingEnabled = false
isVerticalScrollBarEnabled = false isVerticalScrollBarEnabled = false
setHasFixedSize(true) setHasFixedSize(true)
this.adapter = CategoriesAdapter({ categories }) { val adapter = SectionsAdapter({ sections }) {
if (showCategories) { if (showSections) {
showCategories = false showSections = false
category = it section = it
updateCategory() updateSection()
} }
} }
this.adapter = adapter
addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
setBackgroundColor(context.getColorFromAttr(android.R.attr.colorPrimaryDark).defaultColor) setBackgroundColor(context.getColorFromAttr(android.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)
visibility = View.GONE visibility = View.GONE
} }
this.categoriesList = categoriesList this.sectionsList = sectionsList
var lastContentHeight = -1 var lastContentHeight = -1
content.viewTreeObserver.addOnGlobalLayoutListener { content.viewTreeObserver.addOnGlobalLayoutListener {
@@ -280,11 +287,11 @@ class TabsFragment: ScreenFragment() {
if (lastContentHeight != contentHeight) { if (lastContentHeight != contentHeight) {
lastContentHeight = contentHeight lastContentHeight = contentHeight
if (initial) { if (initial) {
categoriesList.layoutParams.height = if (showCategories) contentHeight else 0 sectionsList.layoutParams.height = if (showSections) contentHeight else 0
categoriesList.visibility = if (showCategories) View.VISIBLE else View.GONE sectionsList.visibility = if (showSections) View.VISIBLE else View.GONE
categoriesList.requestLayout() sectionsList.requestLayout()
} else { } else {
animateCategoriesList() animateSectionsList()
} }
} }
} }
@@ -298,7 +305,7 @@ class TabsFragment: ScreenFragment() {
sortOrderMenu = null sortOrderMenu = null
syncRepositoriesMenuItem = null syncRepositoriesMenuItem = null
layout = null layout = null
categoriesList = null sectionsList = null
viewPager = null viewPager = null
syncConnection.unbind(requireContext()) syncConnection.unbind(requireContext())
@@ -306,8 +313,10 @@ class TabsFragment: ScreenFragment() {
sortOrderDisposable = null sortOrderDisposable = null
categoriesDisposable?.dispose() categoriesDisposable?.dispose()
categoriesDisposable = null categoriesDisposable = null
categoriesAnimator?.cancel() repositoriesDisposable?.dispose()
categoriesAnimator = null repositoriesDisposable = null
sectionsAnimator?.cancel()
sectionsAnimator = null
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@@ -315,9 +324,9 @@ class TabsFragment: ScreenFragment() {
outState.putBoolean(STATE_SEARCH_FOCUSED, searchMenuItem?.actionView!!.hasFocus()) outState.putBoolean(STATE_SEARCH_FOCUSED, searchMenuItem?.actionView!!.hasFocus())
outState.putString(STATE_SEARCH_QUERY, searchQuery) outState.putString(STATE_SEARCH_QUERY, searchQuery)
outState.putByte(STATE_SHOW_CATEGORIES, if (showCategories) 1 else 0) outState.putByte(STATE_SHOW_SECTIONS, if (showSections) 1 else 0)
outState.putStringArrayList(STATE_CATEGORIES, ArrayList(categories)) outState.putParcelableArrayList(STATE_SECTIONS, ArrayList(sections))
outState.putString(STATE_CATEGORY, category) outState.putParcelable(STATE_SECTION, section)
} }
override fun onViewStateRestored(savedInstanceState: Bundle?) { override fun onViewStateRestored(savedInstanceState: Bundle?) {
@@ -335,7 +344,7 @@ class TabsFragment: ScreenFragment() {
if (view != null && childFragment is ProductsFragment) { if (view != null && childFragment is ProductsFragment) {
childFragment.setSearchQuery(searchQuery) childFragment.setSearchQuery(searchQuery)
childFragment.setCategory(category) childFragment.setSection(section)
childFragment.setOrder(Preferences[Preferences.Key.SortOrder].order) childFragment.setOrder(Preferences[Preferences.Key.SortOrder].order)
} }
} }
@@ -346,8 +355,8 @@ class TabsFragment: ScreenFragment() {
searchMenuItem?.collapseActionView() searchMenuItem?.collapseActionView()
true true
} }
showCategories -> { showSections -> {
showCategories = false showSections = false
true true
} }
else -> { else -> {
@@ -387,31 +396,52 @@ class TabsFragment: ScreenFragment() {
productFragments.forEach { it.setOrder(order) } productFragments.forEach { it.setOrder(order) }
} }
private fun updateCategory() { private inline fun <reified T: ProductItem.Section> collectOldSections(list: List<T>?): List<T>? {
if (category.isNotEmpty() && categories.indexOf(category) < 0) { val oldList = sections.mapNotNull { it as? T }
category = "" return if (list == null || oldList == list) oldList else null
}
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 fun animateCategoriesList() { private fun setSectionsAndUpdate(categories: List<ProductItem.Section.Category>?,
val categoriesList = categoriesList!! repositories: List<ProductItem.Section.Repository>?) {
val value = if (categoriesList.visibility != View.VISIBLE) 0f else val oldCategories = collectOldSections(categories)
categoriesList.height.toFloat() / (categoriesList.parent as View).height val oldRepositories = collectOldSections(repositories)
val target = if (showCategories) 1f else 0f if (oldCategories == null || oldRepositories == null) {
categoriesAnimator?.cancel() sections = listOf(ProductItem.Section.All) +
categoriesAnimator = null (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) { if (value != target) {
categoriesAnimator = ValueAnimator.ofFloat(value, target).apply { sectionsAnimator = ValueAnimator.ofFloat(value, target).apply {
duration = (250 * abs(target - value)).toLong() duration = (250 * abs(target - value)).toLong()
interpolator = if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f) interpolator = if (target >= 1f) AccelerateInterpolator(2f) else DecelerateInterpolator(2f)
addUpdateListener { addUpdateListener {
val newValue = animatedValue as Float val newValue = animatedValue as Float
categoriesList.apply { sectionsList.apply {
val height = ((parent as View).height * newValue).toInt() val height = ((parent as View).height * newValue).toInt()
val visible = height > 0 val visible = height > 0
if ((visibility == View.VISIBLE) != visible) { if ((visibility == View.VISIBLE) != visible) {
@@ -423,7 +453,7 @@ class TabsFragment: ScreenFragment() {
} }
} }
if (target <= 0f && newValue <= 0f || target >= 1f && newValue >= 1f) { if (target <= 0f && newValue <= 0f || target >= 1f && newValue >= 1f) {
categoriesAnimator = null sectionsAnimator = null
} }
} }
start() start()
@@ -434,24 +464,24 @@ class TabsFragment: ScreenFragment() {
private val pageChangeCallback = object: ViewPager2.OnPageChangeCallback() { private val pageChangeCallback = object: ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
val layout = layout!! val layout = layout!!
val fromCategories = ProductsFragment.Source.values()[position].categories val fromSections = ProductsFragment.Source.values()[position].sections
val toCategories = if (positionOffset <= 0f) fromCategories else val toSections = if (positionOffset <= 0f) fromSections else
ProductsFragment.Source.values()[position + 1].categories ProductsFragment.Source.values()[position + 1].sections
val offset = if (fromCategories != toCategories) { val offset = if (fromSections != toSections) {
if (fromCategories) 1f - positionOffset else positionOffset if (fromSections) 1f - positionOffset else positionOffset
} else { } else {
if (fromCategories) 1f else 0f if (fromSections) 1f else 0f
} }
(layout.tabs.background as TabsBackgroundDrawable) (layout.tabs.background as TabsBackgroundDrawable)
.update(position + positionOffset, layout.tabs.childCount) .update(position + positionOffset, layout.tabs.childCount)
assert(layout.categoryLayout.childCount == 1) assert(layout.sectionLayout.childCount == 1)
val child = layout.categoryLayout.getChildAt(0) val child = layout.sectionLayout.getChildAt(0)
val height = child.layoutParams.height val height = child.layoutParams.height
assert(height > 0) assert(height > 0)
val currentHeight = (offset * height).roundToInt() val currentHeight = (offset * height).roundToInt()
if (layout.categoryLayout.layoutParams.height != currentHeight) { if (layout.sectionLayout.layoutParams.height != currentHeight) {
layout.categoryLayout.layoutParams.height = currentHeight layout.sectionLayout.layoutParams.height = currentHeight
layout.categoryLayout.requestLayout() layout.sectionLayout.requestLayout()
} }
} }
@@ -462,14 +492,14 @@ class TabsFragment: ScreenFragment() {
syncRepositoriesMenuItem!!.setShowAsActionFlags(if (!source.order || syncRepositoriesMenuItem!!.setShowAsActionFlags(if (!source.order ||
resources.configuration.screenWidthDp >= 480) MenuItem.SHOW_AS_ACTION_ALWAYS else 0) resources.configuration.screenWidthDp >= 480) MenuItem.SHOW_AS_ACTION_ALWAYS else 0)
setSelectedTab(source) setSelectedTab(source)
if (showCategories && !source.categories) { if (showSections && !source.sections) {
showCategories = false showSections = false
} }
} }
override fun onPageScrollStateChanged(state: Int) { override fun onPageScrollStateChanged(state: Int) {
val source = ProductsFragment.Source.values()[viewPager!!.currentItem] 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) { if (state == ViewPager2.SCROLL_STATE_IDLE) {
// onPageSelected can be called earlier than fragments created // onPageSelected can be called earlier than fragments created
updateUpdateNotificationBlocker(source) updateUpdateNotificationBlocker(source)
@@ -512,11 +542,12 @@ class TabsFragment: ScreenFragment() {
override fun getOpacity(): Int = PixelFormat.TRANSLUCENT override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
} }
private class CategoriesAdapter(private val categories: () -> List<String>, private val onClick: (String) -> Unit): private class SectionsAdapter(private val sections: () -> List<ProductItem.Section>,
EnumRecyclerAdapter<CategoriesAdapter.ViewType, RecyclerView.ViewHolder>() { private val onClick: (ProductItem.Section) -> Unit): EnumRecyclerAdapter<SectionsAdapter.ViewType,
enum class ViewType { CATEGORY } RecyclerView.ViewHolder>() {
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 val title: TextView
get() = itemView as 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<ViewType> override val viewTypeClass: Class<ViewType>
get() = ViewType::class.java get() = ViewType::class.java
override fun getItemCount(): Int = 1 + categories().size override fun getItemCount(): Int = sections().size
override fun getItemEnumViewType(position: Int): ViewType = ViewType.CATEGORY override fun getItemEnumViewType(position: Int): ViewType = ViewType.SECTION
override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: ViewType): RecyclerView.ViewHolder {
return CategoryViewHolder(parent.context).apply { return SectionViewHolder(parent.context).apply {
itemView.setOnClickListener { onClick(categories().getOrNull(adapterPosition - 1).orEmpty()) } itemView.setOnClickListener { onClick(sections()[adapterPosition]) }
} }
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder as CategoryViewHolder holder as SectionViewHolder
holder.title.text = categories().getOrNull(position - 1) val section = sections()[position]
?: holder.itemView.resources.getString(R.string.all_applications) 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
}
} }
} }
} }
@@ -316,7 +316,7 @@ class SyncService: ConnectionService<SyncService.Binder>() {
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, "", "", ProductItem.Order.NAME, it) .query(true, true, "", ProductItem.Section.All, 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())
+4 -4
View File
@@ -13,12 +13,12 @@
android:orientation="horizontal" /> android:orientation="horizontal" />
<FrameLayout <FrameLayout
android:id="@+id/category_layout" android:id="@+id/section_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp"> android:layout_height="0dp">
<LinearLayout <LinearLayout
android:id="@+id/category_change" android:id="@+id/section_change"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:orientation="horizontal" android:orientation="horizontal"
@@ -29,7 +29,7 @@
tools:ignore="UselessParent"> tools:ignore="UselessParent">
<TextView <TextView
android:id="@+id/category_name" android:id="@+id/section_name"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
@@ -41,7 +41,7 @@
android:textSize="16sp" /> android:textSize="16sp" />
<ImageView <ImageView
android:id="@+id/category_icon" android:id="@+id/section_icon"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginStart="12dp" android:layout_marginStart="12dp"
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<item type="id" name="categories_list" /> <item type="id" name="sections_list" />
<item type="id" name="divider_configuration" /> <item type="id" name="divider_configuration" />
<item type="id" name="fragment_pager" /> <item type="id" name="fragment_pager" />
<item type="id" name="main_content" /> <item type="id" name="main_content" />