Working CategoryBrowseFragment, plus various cleanups to make things more Kotlinesque

This commit is contained in:
casey langen 2019-02-09 22:32:25 -08:00
parent 04f5f2e645
commit 4e0555d13c
10 changed files with 177 additions and 238 deletions

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="io.casey.musikcube.remote" >
xmlns:tools="http://schemas.android.com/tools"
package="io.casey.musikcube.remote">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -10,18 +11,19 @@
<permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
<application
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:name=".Application"
android:usesCleartextTraffic="true"
android:theme="@style/AppTheme" >
android:theme="@style/AppTheme"
tools:replace="android:allowBackup">
<activity android:name=".ui.home.activity.MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
@ -30,6 +32,10 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".ui.category.activity.CategoryBrowseActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".ui.settings.activity.SettingsActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize|stateHidden" />
@ -64,11 +70,6 @@
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="io.casey.musikcube.remote.ui.category.activity.CategoryBrowseActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="io.casey.musikcube.remote.ui.category.activity.AllCategoriesActivity"
android:screenOrientation="portrait"
@ -79,7 +80,9 @@
android:label="@string/connections_title"
android:windowSoftInputMode="adjustResize" />
<service android:name="io.casey.musikcube.remote.service.system.SystemService">
<service
android:name="io.casey.musikcube.remote.service.system.SystemService"
android:exported="false">
<intent-filter>
<action android:name="io.casey.musikcube.remote.WAKE_UP" />
<action android:name="io.casey.musikcube.remote.SHUT_DOWN" />

View File

@ -61,11 +61,9 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
emptyView.emptyMessage = getString(R.string.empty_no_items_format, getString(R.string.browse_type_albums))
emptyView.alternateView = recyclerView
transport = addTransportFragment(object: TransportFragment.OnModelChangedListener {
override fun onChanged(fragment: TransportFragment) {
adapter.notifyDataSetChanged()
}
})!!
transport = addTransportFragment {
adapter.notifyDataSetChanged()
}
}
override fun onResume() {

View File

@ -4,188 +4,51 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.View
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
import io.casey.musikcube.remote.ui.category.adapter.CategoryBrowseAdapter
import io.casey.musikcube.remote.ui.category.constant.*
import io.casey.musikcube.remote.ui.category.constant.Category
import io.casey.musikcube.remote.ui.category.constant.NavigationType
import io.casey.musikcube.remote.ui.category.fragment.CategoryBrowseFragment
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.activity.Filterable
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.shared.extension.findFragment
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
import io.casey.musikcube.remote.util.Debouncer
import io.reactivex.rxkotlin.subscribeBy
import java.lang.IllegalArgumentException
import io.casey.musikcube.remote.service.websocket.WebSocketService.State as SocketState
class CategoryBrowseActivity : BaseActivity(), Filterable {
private lateinit var adapter: CategoryBrowseAdapter
private var navigationType: NavigationType = NavigationType.Tracks
private var lastFilter: String? = null
private lateinit var category: String
private lateinit var predicateType: String
private var predicateId: Long = -1
private lateinit var content: CategoryBrowseFragment
private lateinit var transport: TransportFragment
private lateinit var emptyView: EmptyListView
private lateinit var data: DataProviderMixin
private lateinit var playback: PlaybackMixin
override fun onCreate(savedInstanceState: Bundle?) {
component.inject(this)
data = mixin(DataProviderMixin())
playback = mixin(PlaybackMixin())
mixin(ItemContextMenuMixin(this, contextMenuListener))
super.onCreate(savedInstanceState)
category = intent.getStringExtra(Category.Extra.CATEGORY)
predicateType = intent.getStringExtra(Category.Extra.PREDICATE_TYPE) ?: ""
predicateId = intent.getLongExtra(Category.Extra.PREDICATE_ID, -1)
navigationType = NavigationType.get(intent.getIntExtra(Category.Extra.NAVIGATION_TYPE, NavigationType.Albums.ordinal))
adapter = CategoryBrowseAdapter(adapterListener, playback, navigationType, category, prefs)
setContentView(R.layout.fragment_with_transport_activity)
setContentView(R.layout.recycler_view_activity)
setTitleFromIntent(categoryTitleString)
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
val fab = findViewById<View>(R.id.fab)
val fabVisible = (category == Messages.Category.PLAYLISTS)
emptyView = findViewById(R.id.empty_list_view)
emptyView.capability = EmptyListView.Capability.OnlineOnly
emptyView.emptyMessage = getString(R.string.empty_no_items_format, categoryTypeString)
emptyView.alternateView = recyclerView
setupDefaultRecyclerView(recyclerView, adapter)
setFabVisible(fabVisible, fab, recyclerView)
enableUpNavigation()
findViewById<View>(R.id.fab).setOnClickListener {
if (category == Messages.Category.PLAYLISTS) {
mixin(ItemContextMenuMixin::class.java)?.createPlaylist()
}
when (savedInstanceState == null) {
true -> createFragments()
else -> restoreFragments()
}
transport = addTransportFragment(object: TransportFragment.OnModelChangedListener {
override fun onChanged(fragment: TransportFragment) {
adapter.notifyDataSetChanged()
}
})!!
}
override fun onResume() {
super.onResume()
initObservers()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
if (Messages.Category.PLAYLISTS != category) { /* bleh */
initSearchMenu(menu, this)
}
return true
}
override fun setFilter(filter: String) {
this.lastFilter = filter
this.filterDebouncer.call()
}
private fun initObservers() {
disposables.add(data.provider.observeState().subscribeBy(
onNext = { states ->
when (states.first) {
IDataProvider.State.Connected -> {
filterDebouncer.cancel()
requery()
}
IDataProvider.State.Disconnected -> {
emptyView.update(states.first, adapter.itemCount)
}
else -> { }
}
},
onError = {
}))
}
private val categoryTypeString: String
get() {
Category.NAME_TO_EMPTY_TYPE[category]?.let {
return getString(it)
}
return Category.toDisplayString(this, category)
}
private val categoryTitleString: String
get() {
Category.NAME_TO_TITLE[category]?.let {
return getString(it)
}
return Category.toDisplayString(this, category)
}
private fun requery() {
@Suppress("UNUSED")
data.provider
.getCategoryValues(category, predicateType, predicateId, lastFilter ?: "")
.subscribeBy(
onNext = { values -> adapter.setModel(values) },
onError = { },
onComplete = { emptyView.update(data.provider.state, adapter.itemCount)})
}
private val filterDebouncer = object : Debouncer<String>(350) {
override fun onDebounced(last: String?) {
if (!isPaused()) {
requery()
}
transport.modelChangedListener = {
content.notifyTransportChanged()
}
}
private val contextMenuListener = object: ItemContextMenuMixin.EventListener() {
override fun onPlaylistDeleted(id: Long, name: String) = requery()
override fun onCreateOptionsMenu(menu: Menu): Boolean = content.createOptionsMenu(menu)
override fun setFilter(filter: String) = content.setFilter(filter)
override fun onPlaylistUpdated(id: Long, name: String) = requery()
override fun onPlaylistCreated(id: Long, name: String) =
if (navigationType == NavigationType.Select) navigateToSelect(id, name) else requery()
private fun createFragments() {
content = CategoryBrowseFragment.create(intent)
transport = TransportFragment.create()
supportFragmentManager
.beginTransaction()
.add(R.id.content_container, content, CategoryBrowseFragment.TAG)
.add(R.id.transport_container, transport, TransportFragment.TAG)
.commit()
}
private val adapterListener = object: CategoryBrowseAdapter.EventListener {
override fun onItemClicked(value: ICategoryValue) {
when (navigationType) {
NavigationType.Albums -> navigateToAlbums(value)
NavigationType.Tracks -> navigateToTracks(value)
NavigationType.Select -> navigateToSelect(value.id, value.value)
}
}
override fun onActionClicked(view: View, value: ICategoryValue) {
mixin(ItemContextMenuMixin::class.java)?.showForCategory(value, view)
}
}
private fun navigateToAlbums(entry: ICategoryValue) =
startActivity(AlbumBrowseActivity.getStartIntent(this, category, entry))
private fun navigateToTracks(entry: ICategoryValue) =
startActivity(TrackListActivity.getStartIntent(this, category, entry.id, entry.value))
private fun navigateToSelect(id: Long, name: String) {
setResult(RESULT_OK, Intent()
.putExtra(Category.Extra.CATEGORY, category)
.putExtra(Category.Extra.ID, id)
.putExtra(Category.Extra.NAME, name))
finish()
private fun restoreFragments() {
transport = findFragment(TransportFragment.TAG)
content = findFragment(CategoryBrowseFragment.TAG)
}
companion object {
@ -193,33 +56,26 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
category: String,
predicateType: String = "",
predicateId: Long = -1,
predicateValue: String = ""): Intent
{
val intent = Intent(context, CategoryBrowseActivity::class.java)
.putExtra(Category.Extra.CATEGORY, category)
.putExtra(Category.Extra.PREDICATE_TYPE, predicateType)
.putExtra(Category.Extra.PREDICATE_ID, predicateId)
if (predicateValue.isNotBlank() && Category.NAME_TO_RELATED_TITLE.containsKey(category)) {
val format = Category.NAME_TO_RELATED_TITLE[category]
when (format) {
null -> throw IllegalArgumentException("unknown category $category")
else -> intent.putExtra(EXTRA_ACTIVITY_TITLE, context.getString(format, predicateValue))
}
predicateValue: String = ""): Intent =
Intent(context, CategoryBrowseActivity::class.java).apply {
putExtra(
Category.Extra.FRAGMENT_ARGUMENTS,
CategoryBrowseFragment.arguments(
context,
category,
predicateType,
predicateId,
predicateValue))
}
return intent
}
fun getStartIntent(context: Context,
category: String,
navigationType: NavigationType,
title: String = ""): Intent
{
return Intent(context, CategoryBrowseActivity::class.java)
.putExtra(Category.Extra.CATEGORY, category)
.putExtra(Category.Extra.NAVIGATION_TYPE, navigationType.ordinal)
.putExtra(Category.Extra.TITLE, title)
}
title: String = ""): Intent =
Intent(context, CategoryBrowseActivity::class.java).apply {
putExtra(
Category.Extra.FRAGMENT_ARGUMENTS,
CategoryBrowseFragment.arguments(category, navigationType, title))
}
}
}

View File

@ -53,6 +53,7 @@ object Category {
const val PREDICATE_ID = "extra_predicate_id"
const val NAVIGATION_TYPE = "extra_navigation_type"
const val TITLE = "extra_title"
const val FRAGMENT_ARGUMENTS = "fragment_arguments"
}
}

View File

@ -68,10 +68,10 @@ class CategoryBrowseFragment: BaseFragment(), Filterable {
adapter = CategoryBrowseAdapter(adapterListener, playback, navigationType, category, prefs)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
rootView = inflater.inflate(R.layout.recycler_view_activity, container, false)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
inflater.inflate(R.layout.recycler_view_fragment, container, false).apply {
this@CategoryBrowseFragment.rootView = this
rootView.apply {
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
val fab = findViewById<View>(R.id.fab)
val fabVisible = (category == Messages.Category.PLAYLISTS)
@ -91,9 +91,6 @@ class CategoryBrowseFragment: BaseFragment(), Filterable {
setFabVisible(fabVisible, fab, recyclerView)
}
return rootView
}
override fun onResume() {
super.onResume()
initObservers()
@ -112,6 +109,9 @@ class CategoryBrowseFragment: BaseFragment(), Filterable {
return true
}
fun notifyTransportChanged() =
adapter.notifyDataSetChanged()
private fun initObservers() {
disposables.add(data.provider.observeState().subscribeBy(
onNext = { states ->
@ -196,24 +196,45 @@ class CategoryBrowseFragment: BaseFragment(), Filterable {
}
companion object {
fun create(context: Context,
category: String,
predicateType: String = "",
predicateId: Long = -1,
predicateValue: String = ""): CategoryBrowseFragment =
const val TAG = "CategoryBrowseFragment"
fun create(intent: Intent?): CategoryBrowseFragment {
if (intent == null) {
throw IllegalArgumentException("invalid intent")
}
return create(intent.getBundleExtra(Category.Extra.FRAGMENT_ARGUMENTS))
}
fun create(arguments: Bundle): CategoryBrowseFragment =
CategoryBrowseFragment().apply {
this.arguments = Bundle().apply {
putString(Category.Extra.CATEGORY, category)
putString(Category.Extra.PREDICATE_TYPE, predicateType)
putLong(Category.Extra.PREDICATE_ID, predicateId)
if (predicateValue.isNotBlank() && Category.NAME_TO_RELATED_TITLE.containsKey(category)) {
val format = Category.NAME_TO_RELATED_TITLE[category]
when (format) {
null -> throw IllegalArgumentException("unknown category $category")
else -> putString(EXTRA_ACTIVITY_TITLE, context.getString(format, predicateValue))
}
this.arguments = arguments
}
fun arguments(context: Context,
category: String,
predicateType: String = "",
predicateId: Long = -1,
predicateValue: String = ""): Bundle =
Bundle().apply {
putString(Category.Extra.CATEGORY, category)
putString(Category.Extra.PREDICATE_TYPE, predicateType)
putLong(Category.Extra.PREDICATE_ID, predicateId)
if (predicateValue.isNotBlank() && Category.NAME_TO_RELATED_TITLE.containsKey(category)) {
val format = Category.NAME_TO_RELATED_TITLE[category]
when (format) {
null -> throw IllegalArgumentException("unknown category $category")
else -> putString(EXTRA_ACTIVITY_TITLE, context.getString(format, predicateValue))
}
}
}
fun arguments(category: String,
navigationType: NavigationType,
title: String = ""): Bundle =
Bundle().apply {
putString(Category.Extra.CATEGORY, category)
putInt(Category.Extra.NAVIGATION_TYPE, navigationType.ordinal)
putString(Category.Extra.TITLE, title)
}
}
}

View File

@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.support.design.widget.Snackbar
import android.support.v4.app.DialogFragment
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.content.ContextCompat
import android.support.v4.view.MenuItemCompat
import android.support.v7.app.AppCompatActivity
@ -28,6 +29,7 @@ import io.casey.musikcube.remote.ui.shared.activity.Filterable
import io.casey.musikcube.remote.ui.shared.fragment.BaseFragment
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
import io.casey.musikcube.remote.util.Strings
import java.lang.IllegalArgumentException
const val EXTRA_ACTIVITY_TITLE = "extra_title"
@ -65,26 +67,25 @@ fun AppCompatActivity.enableUpNavigation() {
}
fun AppCompatActivity.addTransportFragment(
listener: TransportFragment.OnModelChangedListener? = null): TransportFragment?
listener: ((TransportFragment) -> Unit)? = null): TransportFragment
{
val root = findViewById<View>(android.R.id.content)
if (root != null) {
if (root.findViewById<View>(R.id.transport_container) != null) {
val fragment = TransportFragment.newInstance()
val fragment = TransportFragment.create()
this.supportFragmentManager
.beginTransaction()
.add(R.id.transport_container, fragment, TransportFragment.TAG)
.commit()
.beginTransaction()
.add(R.id.transport_container, fragment, TransportFragment.TAG)
.commit()
fragment.modelChangedListener = listener
return fragment
}
}
return null
throw IllegalArgumentException("could not find content view")
}
fun AppCompatActivity.setTitleFromIntent(defaultId: Int) =
this.setTitleFromIntent(getString(defaultId))
@ -274,4 +275,12 @@ fun titleEllipsizeMode(prefs: SharedPreferences): TextUtils.TruncateAt {
1 -> TextUtils.TruncateAt.MIDDLE
else -> TextUtils.TruncateAt.END
}
}
inline fun <reified T> FragmentManager.find(tag: String): T {
return findFragmentByTag(tag) as T
}
inline fun <reified T> AppCompatActivity.findFragment(tag: String): T {
return this.supportFragmentManager.find(tag)
}

View File

@ -23,9 +23,7 @@ class TransportFragment: BaseFragment() {
lateinit var playback: PlaybackMixin
private set
interface OnModelChangedListener {
fun onChanged(fragment: TransportFragment)
}
var modelChangedListener: ((TransportFragment) -> Unit)? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
@ -46,8 +44,6 @@ class TransportFragment: BaseFragment() {
rebindUi()
}
var modelChangedListener: OnModelChangedListener? = null
private fun bindEventHandlers() {
this.title = this.rootView.findViewById(R.id.track_title)
this.title.isClickable = false
@ -119,11 +115,11 @@ class TransportFragment: BaseFragment() {
private val playbackListener: () -> Unit = {
rebindUi()
modelChangedListener?.onChanged(this@TransportFragment)
modelChangedListener?.invoke(this@TransportFragment)
}
companion object {
const val TAG = "TransportFragment"
fun newInstance(): TransportFragment = TransportFragment()
fun create(): TransportFragment = TransportFragment()
}
}

View File

@ -82,10 +82,9 @@ class TrackListActivity : BaseActivity(), Filterable {
tracks.setOnMetadataLoadedListener(slidingWindowListener)
transport = addTransportFragment(object: TransportFragment.OnModelChangedListener {
override fun onChanged(fragment: TransportFragment) =
adapter.notifyDataSetChanged()
})!!
transport = addTransportFragment {
adapter.notifyDataSetChanged()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/content_container"
android:layout_weight="1"
android:layout_height="0dp"
android:layout_width="match_parent" />
<FrameLayout
android:id="@+id/transport_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"/>
</LinearLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
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_height="match_parent">
<com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fastScrollAutoHide="true"
app:fastScrollAutoHideDelay="1500"
app:fastScrollThumbColor="@color/color_accent" />
<io.casey.musikcube.remote.ui.shared.view.EmptyListView
android:id="@+id/empty_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_gravity="bottom|right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/fab_padding"
android:src="@drawable/ic_fab_add"
android:scaleType="center"
android:elevation="6dp"
android:visibility="gone"
app:backgroundTint="@color/color_primary"
app:fabSize="mini"
app:rippleColor="?colorAccent"/>
</FrameLayout>