diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt new file mode 100644 index 00000000..892186fa --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/ExploreFragment.kt @@ -0,0 +1,94 @@ +package com.looker.droidify.ui.fragments + +import android.database.Cursor +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import com.looker.droidify.R +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.database.Database +import com.looker.droidify.databinding.FragmentExploreXBinding +import com.looker.droidify.ui.adapters.AppListAdapter +import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX +import com.looker.droidify.utility.RxUtils +import com.looker.droidify.widget.RecyclerFastScroller +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class ExploreFragment : MainNavFragmentX(), CursorOwner.Callback { + + override val viewModel: MainNavFragmentViewModelX by viewModels() + private lateinit var binding: FragmentExploreXBinding + + override val source = Source.AVAILABLE + + private var repositoriesDisposable: Disposable? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + super.onCreate(savedInstanceState) + binding = FragmentExploreXBinding.inflate(inflater, container, false) + binding.lifecycleOwner = this + + binding.recyclerView.apply { + layoutManager = LinearLayoutManager(context) + isMotionEventSplittingEnabled = false + isVerticalScrollBarEnabled = false + setHasFixedSize(true) + recycledViewPool.setMaxRecycledViews(AppListAdapter.ViewType.PRODUCT.ordinal, 30) + adapter = AppListAdapter { mainActivityX.navigateProduct(it.packageName) } + RecyclerFastScroller(this) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + mainActivityX.attachCursorOwner(this, viewModel.request(source)) + repositoriesDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it } + } + + override fun onDestroyView() { + super.onDestroyView() + + mainActivityX.detachCursorOwner(this) + repositoriesDisposable?.dispose() + repositoriesDisposable = null + } + + override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { + (binding.recyclerView.adapter as? AppListAdapter)?.apply { + this.cursor = cursor + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + emptyText = when { + cursor == null -> "" + viewModel.searchQuery.first() + .isNotEmpty() -> getString(R.string.no_matching_applications_found) + else -> getString(R.string.no_applications_available) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt new file mode 100644 index 00000000..ea427cc6 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/InstalledFragment.kt @@ -0,0 +1,93 @@ +package com.looker.droidify.ui.fragments + +import android.database.Cursor +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import com.looker.droidify.R +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.database.Database +import com.looker.droidify.databinding.FragmentInstalledXBinding +import com.looker.droidify.ui.adapters.AppListAdapter +import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX +import com.looker.droidify.utility.RxUtils +import com.looker.droidify.widget.RecyclerFastScroller +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class InstalledFragment : MainNavFragmentX(), CursorOwner.Callback { + + override val viewModel: MainNavFragmentViewModelX by viewModels() + private lateinit var binding: FragmentInstalledXBinding + + override val source = Source.INSTALLED + + private var repositoriesDisposable: Disposable? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + super.onCreate(savedInstanceState) + binding = FragmentInstalledXBinding.inflate(inflater, container, false) + binding.lifecycleOwner = this + + binding.recyclerView.apply { + layoutManager = LinearLayoutManager(context) + isMotionEventSplittingEnabled = false + isVerticalScrollBarEnabled = false + adapter = AppListAdapter { mainActivityX.navigateProduct(it.packageName) } + RecyclerFastScroller(this) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + mainActivityX.attachCursorOwner(this, viewModel.request(source)) + repositoriesDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it } + } + + override fun onDestroyView() { + super.onDestroyView() + + mainActivityX.detachCursorOwner(this) + repositoriesDisposable?.dispose() + repositoriesDisposable = null + } + + override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { + // TODO create app list out of cursor and use those on the different RecycleViews + (binding.recyclerView.adapter as? AppListAdapter)?.apply { + this.cursor = cursor + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + emptyText = when { + cursor == null -> "" + viewModel.searchQuery.first() + .isNotEmpty() -> getString(R.string.no_matching_applications_found) + else -> getString(R.string.all_applications_up_to_date) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt new file mode 100644 index 00000000..778eb7c6 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/LatestFragment.kt @@ -0,0 +1,94 @@ +package com.looker.droidify.ui.fragments + +import android.database.Cursor +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.LinearLayoutManager +import com.looker.droidify.R +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.database.Database +import com.looker.droidify.databinding.FragmentLatestXBinding +import com.looker.droidify.ui.adapters.AppListAdapter +import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX +import com.looker.droidify.utility.RxUtils +import com.looker.droidify.widget.RecyclerFastScroller +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class LatestFragment : MainNavFragmentX(), CursorOwner.Callback { + + override val viewModel: MainNavFragmentViewModelX by viewModels() + private lateinit var binding: FragmentLatestXBinding + + override val source = Source.UPDATES + + private var repositoriesDisposable: Disposable? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + super.onCreate(savedInstanceState) + binding = FragmentLatestXBinding.inflate(inflater, container, false) + binding.lifecycleOwner = this + + binding.recyclerView.apply { + id = android.R.id.list + layoutManager = LinearLayoutManager(context) + isMotionEventSplittingEnabled = false + isVerticalScrollBarEnabled = false + adapter = AppListAdapter { mainActivityX.navigateProduct(it.packageName) } + RecyclerFastScroller(this) + } + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + mainActivityX.attachCursorOwner(this, viewModel.request(source)) + repositoriesDisposable = Observable.just(Unit) + .concatWith(Database.observable(Database.Subject.Repositories)) + .observeOn(Schedulers.io()) + .flatMapSingle { RxUtils.querySingle { Database.RepositoryAdapter.getAll(it) } } + .map { list -> list.asSequence().map { Pair(it.id, it) }.toMap() } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { (binding.recyclerView.adapter as? AppListAdapter)?.repositories = it } + } + + override fun onDestroyView() { + super.onDestroyView() + + mainActivityX.detachCursorOwner(this) + repositoriesDisposable?.dispose() + repositoriesDisposable = null + } + + override fun onCursorData(request: CursorOwner.Request, cursor: Cursor?) { + // TODO create app list out of cursor and use those on the different RecycleViews + (binding.recyclerView.adapter as? AppListAdapter)?.apply { + this.cursor = cursor + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + emptyText = when { + cursor == null -> "" + viewModel.searchQuery.first() + .isNotEmpty() -> getString(R.string.no_matching_applications_found) + else -> getString(R.string.all_applications_up_to_date) + } + } + } + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt new file mode 100644 index 00000000..3abcb73c --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/MainNavFragmentX.kt @@ -0,0 +1,47 @@ +package com.looker.droidify.ui.fragments + +import androidx.fragment.app.Fragment +import com.looker.droidify.R +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.entity.ProductItem +import com.looker.droidify.ui.activities.MainActivityX +import com.looker.droidify.ui.viewmodels.MainNavFragmentViewModelX + +abstract class MainNavFragmentX : Fragment(), CursorOwner.Callback { + val mainActivityX: MainActivityX + get() = requireActivity() as MainActivityX + abstract val viewModel: MainNavFragmentViewModelX + abstract val source: Source + + open fun onBackPressed(): Boolean = false + + internal fun setSearchQuery(searchQuery: String) { + viewModel.setSearchQuery(searchQuery) { + if (view != null) { + mainActivityX.attachCursorOwner(this, viewModel.request(source)) + } + } + } + + internal fun setSection(section: ProductItem.Section) { + viewModel.setSection(section) { + if (view != null) { + mainActivityX.attachCursorOwner(this, viewModel.request(source)) + } + } + } + + internal fun setOrder(order: ProductItem.Order) { + viewModel.setOrder(order) { + if (view != null) { + mainActivityX.attachCursorOwner(this, viewModel.request(source)) + } + } + } +} + +enum class Source(val titleResId: Int, val sections: Boolean, val order: Boolean) { + AVAILABLE(R.string.available, true, true), + INSTALLED(R.string.installed, false, true), + UPDATES(R.string.updates, false, false) +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt new file mode 100644 index 00000000..30832c56 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/ui/viewmodels/MainNavFragmentViewModelX.kt @@ -0,0 +1,89 @@ +package com.looker.droidify.ui.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.looker.droidify.database.CursorOwner +import com.looker.droidify.entity.ProductItem +import com.looker.droidify.ui.fragments.Source +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +class MainNavFragmentViewModelX : ViewModel() { + + private val _order = MutableStateFlow(ProductItem.Order.LAST_UPDATE) + private val _sections = MutableStateFlow(ProductItem.Section.All) + private val _searchQuery = MutableStateFlow("") + + val order: StateFlow = _order.stateIn( + initialValue = ProductItem.Order.LAST_UPDATE, + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000) + ) + + val sections: StateFlow = _sections.stateIn( + initialValue = ProductItem.Section.All, + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000) + ) + val searchQuery: StateFlow = _searchQuery.stateIn( + initialValue = "", + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000) + ) + + fun request(source: Source): CursorOwner.Request { + var mSearchQuery = "" + var mSections: ProductItem.Section = ProductItem.Section.All + var mOrder: ProductItem.Order = ProductItem.Order.NAME + viewModelScope.launch { + launch { searchQuery.collect { if (source.sections) mSearchQuery = it } } + launch { sections.collect { if (source.sections) mSections = it } } + launch { order.collect { if (source.order) mOrder = it } } + } + return when (source) { + Source.AVAILABLE -> CursorOwner.Request.ProductsAvailable( + mSearchQuery, + mSections, + mOrder + ) + Source.INSTALLED -> CursorOwner.Request.ProductsInstalled( + mSearchQuery, + mSections, + mOrder + ) + Source.UPDATES -> CursorOwner.Request.ProductsUpdates( + mSearchQuery, + mSections, + mOrder + ) + } + } + + fun setSection(newSection: ProductItem.Section, perform: () -> Unit) { + viewModelScope.launch { + if (newSection != sections.value) { + _sections.emit(newSection) + launch(Dispatchers.Main) { perform() } + } + } + } + + fun setOrder(newOrder: ProductItem.Order, perform: () -> Unit) { + viewModelScope.launch { + if (newOrder != order.value) { + _order.emit(newOrder) + launch(Dispatchers.Main) { perform() } + } + } + } + + fun setSearchQuery(newSearchQuery: String, perform: () -> Unit) { + viewModelScope.launch { + if (newSearchQuery != searchQuery.value) { + _searchQuery.emit(newSearchQuery) + launch(Dispatchers.Main) { perform() } + } + } + } +} \ No newline at end of file