Intermediate commit with a bunch of important changes:

1. Renamed "Components" to "Mixins" to avoid confusion with Dagger
2. Implemented a bunch of mixins -- DataProviderMixin,
ItemContextMenuMixin, PlaybackMixin, RunnerMixin, ViewModelMixin to
modularize functionality and slim down base classes
3. Started implementing new context menu for browsing predicated
categories
4. Some other general cleanup -- more properties instead of methods,
function expressions, etc
This commit is contained in:
casey langen 2017-11-17 21:40:41 -08:00
parent b950ae0259
commit 6573194dac
44 changed files with 1427 additions and 749 deletions

View File

@ -19,7 +19,7 @@ android {
defaultConfig {
applicationId "io.casey.musikcube.remote"
minSdkVersion 16
minSdkVersion 21
targetSdkVersion 26
versionCode 23
versionName "0.15.3"
@ -72,6 +72,10 @@ dependencies {
implementation "android.arch.persistence.room:runtime:1.0.0"
kapt "android.arch.persistence.room:compiler:1.0.0"
implementation "android.arch.lifecycle:runtime:1.0.3"
implementation "android.arch.lifecycle:extensions:1.0.0"
kapt "android.arch.lifecycle:compiler:1.0.0"
compileOnly 'org.glassfish:javax.annotation:10.0-b28'
implementation 'com.google.dagger:dagger:2.11'
kapt 'com.google.dagger:dagger-compiler:2.11'
@ -81,8 +85,9 @@ dependencies {
implementation 'com.github.bumptech.glide:glide:4.3.1'
implementation "com.github.bumptech.glide:okhttp3-integration:4.3.1"
kapt 'com.github.bumptech.glide:compiler:4.3.1'
implementation 'io.reactivex.rxjava2:rxjava:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.1.6'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.1'
implementation 'io.reactivex.rxjava2:rxkotlin:2.1.0'
implementation 'com.google.android.exoplayer:exoplayer:r2.4.2'
implementation 'com.google.android.exoplayer:extension-okhttp:r2.4.2'
implementation 'com.simplecityapps:recyclerview-fastscroll:1.0.16'

View File

@ -1,13 +1,15 @@
package io.casey.musikcube.remote.framework.components
package io.casey.musikcube.remote.framework
import android.content.Intent
import android.os.Bundle
interface IComponent {
interface IMixin {
fun onCreate(bundle: Bundle)
fun onStart()
fun onResume()
fun onPause()
fun onStop()
fun onSaveInstanceState(bundle: Bundle)
fun onActivityResult(request: Int, result: Int, data: Intent?)
fun onDestroy()
}

View File

@ -1,8 +1,10 @@
package io.casey.musikcube.remote.framework.components
package io.casey.musikcube.remote.framework
import android.content.Intent
import android.os.Bundle
import io.casey.musikcube.remote.Application
abstract class ComponentBase: IComponent {
abstract class MixinBase : IMixin {
enum class State {
Unknown, Created, Started, Resumed, Paused, Stopped, Destroyed
}
@ -10,6 +12,12 @@ abstract class ComponentBase: IComponent {
protected var state = State.Unknown
private set
protected val active
get() = state == State.Resumed
protected var context = Application.instance!!
private set
override fun onCreate(bundle: Bundle) {
state = State.Created
}
@ -30,6 +38,9 @@ abstract class ComponentBase: IComponent {
state = State.Stopped
}
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
}
override fun onSaveInstanceState(bundle: Bundle) {
}

View File

@ -0,0 +1,92 @@
package io.casey.musikcube.remote.framework
import android.content.Intent
import android.os.Bundle
class MixinSet : MixinBase() {
private data class ActivityResult (val request: Int, val result: Int, val data: Intent?)
private var activityResult: ActivityResult? = null
private val components: MutableMap<Class<out IMixin>, IMixin> = mutableMapOf()
private var bundle = Bundle()
fun <T> add(mixin: IMixin): T {
components.put(mixin.javaClass, mixin)
when (state) {
State.Created ->
mixin.onCreate(bundle)
State.Started -> {
mixin.onCreate(bundle)
mixin.onStart()
}
State.Resumed -> {
mixin.onCreate(bundle)
mixin.onStart()
mixin.onResume()
}
State.Paused -> {
mixin.onCreate(bundle)
mixin.onStart()
}
else -> {
}
}
return mixin as T
}
fun <T: IMixin> get(cls: Class<out T>): T? = components.get(cls) as T?
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)
this.bundle = bundle
components.values.forEach { it.onCreate(bundle) }
}
override fun onStart() {
super.onStart()
components.values.forEach { it.onStart() }
}
override fun onResume() {
super.onResume()
components.values.forEach { it.onResume() }
val ar = activityResult
if (ar != null) {
components.values.forEach { it.onActivityResult(ar.request, ar.result, ar.data) }
activityResult = null
}
}
override fun onPause() {
super.onPause()
components.values.forEach { it.onPause() }
}
override fun onStop() {
super.onStop()
components.values.forEach { it.onStop() }
}
override fun onSaveInstanceState(bundle: Bundle) {
super.onSaveInstanceState(bundle)
components.values.forEach { it.onSaveInstanceState(bundle) }
}
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
super.onActivityResult(request, result, data)
if (active) {
components.values.forEach { it.onActivityResult(request, result, data) }
}
else {
activityResult = ActivityResult(request, result, data)
}
}
override fun onDestroy() {
super.onDestroy()
components.values.forEach { it.onDestroy() }
}
}

View File

@ -0,0 +1,65 @@
package io.casey.musikcube.remote.framework
import android.content.Context
import android.os.Handler
import android.os.Looper
import com.uacf.taskrunner.Runner
import com.uacf.taskrunner.Task
import io.casey.musikcube.remote.Application
import java.util.concurrent.atomic.AtomicLong
abstract class ViewModel<ListenerT>(protected val runner: Runner? = null): Runner.TaskCallbacks {
val id: Long = nextId.incrementAndGet()
interface Provider {
fun <T: ViewModel<*>> createViewModel(): T?
}
protected var listener: ListenerT? = null
private set
fun onPause() {
}
fun onResume() {
}
fun onDestroy() {
listener = null
handler.postDelayed(cleanup, cleanupDelayMs)
}
fun observe(listener: ListenerT) {
this.listener = listener
}
val context: Context = Application.instance!!
internal val cleanup = object: Runnable {
override fun run() {
listener = null
idToInstance.remove(id)
}
}
override fun onTaskError(name: String?, id: Long, task: Task<*, *>?, error: Throwable?) {
}
override fun onTaskCompleted(name: String?, id: Long, task: Task<*, *>?, result: Any?) {
}
companion object {
private val cleanupDelayMs = 3000L
private val nextId = AtomicLong(System.currentTimeMillis() + 0)
private val handler by lazy { Handler(Looper.getMainLooper()) }
private val idToInstance = mutableMapOf<Long, ViewModel<*>>()
fun <T: ViewModel<*>> restore(id: Long): T? {
val instance: T? = idToInstance[id] as T?
if (instance != null) {
handler.removeCallbacks(instance.cleanup)
}
return instance
}
}
}

View File

@ -1,70 +0,0 @@
package io.casey.musikcube.remote.framework.components
import android.os.Bundle
class ComponentSet : ComponentBase() {
private val components: MutableMap<Class<out IComponent>, IComponent> = mutableMapOf()
private var bundle = Bundle()
fun add(component: IComponent) {
components.put(component.javaClass, component)
when (state) {
State.Created ->
component.onCreate(bundle)
State.Started -> {
component.onCreate(bundle)
component.onStart()
}
State.Resumed -> {
component.onCreate(bundle)
component.onStart()
component.onResume()
}
State.Paused -> {
component.onCreate(bundle)
component.onStart()
}
else -> {
}
}
}
fun <T> get(cls: Class<out IComponent>): T? = components.get(cls) as T
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)
this.bundle = bundle
components.values.forEach { it.onCreate(bundle) }
}
override fun onStart() {
super.onStart()
components.values.forEach { it.onStart() }
}
override fun onResume() {
super.onResume()
components.values.forEach { it.onResume() }
}
override fun onPause() {
super.onPause()
components.values.forEach { it.onPause() }
}
override fun onStop() {
super.onStop()
components.values.forEach { it.onStop() }
}
override fun onSaveInstanceState(bundle: Bundle) {
super.onSaveInstanceState(bundle)
components.values.forEach { it.onSaveInstanceState(bundle) }
}
override fun onDestroy() {
super.onDestroy()
components.values.forEach { it.onDestroy() }
}
}

View File

@ -10,6 +10,8 @@ import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.casey.musikcube.remote.ui.home.view.MainMetadataView
import io.casey.musikcube.remote.ui.settings.activity.ConnectionsActivity
import io.casey.musikcube.remote.ui.settings.activity.SettingsActivity
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
@ViewScope
@ -25,7 +27,11 @@ interface ViewComponent {
fun inject(activity: CategoryBrowseActivity)
fun inject(activity: PlayQueueActivity)
fun inject(activity: TrackListActivity)
fun inject(view: EmptyListView)
fun inject(view: MainMetadataView)
fun inject(mixin: DataProviderMixin)
fun inject(mixin: ItemContextMenuMixin)
}

View File

@ -34,13 +34,13 @@ interface IPlaybackService {
val currentTime: Double
val bufferedTime: Double
val playbackState: PlaybackState
val state: PlaybackState
fun toggleShuffle()
val isShuffled: Boolean
val shuffled: Boolean
fun toggleMute()
val isMuted: Boolean
val muted: Boolean
fun toggleRepeatMode()
val repeatMode: RepeatMode

View File

@ -2,18 +2,18 @@ package io.casey.musikcube.remote.service.playback.impl.remote
import android.os.Handler
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.model.impl.remote.RemoteTrack
import io.casey.musikcube.remote.injection.DaggerServiceComponent
import io.casey.musikcube.remote.injection.DataModule
import io.casey.musikcube.remote.model.impl.remote.RemoteTrack
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackState
import io.casey.musikcube.remote.service.playback.RepeatMode
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.SocketMessage
import io.casey.musikcube.remote.service.websocket.WebSocketService
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.reactivex.Observable
import org.json.JSONObject
import java.util.*
@ -49,12 +49,7 @@ class RemotePlaybackService : IPlaybackService {
internal fun get(track: JSONObject?): Double {
if (track != null && track.optLong(Metadata.Track.ID, -1L) == trackId && trackId != -1L) {
if (pauseTime != 0.0) {
return pauseTime
}
else {
return estimatedTime()
}
return if (pauseTime != 0.0) pauseTime else estimatedTime()
}
return 0.0
}
@ -106,27 +101,25 @@ class RemotePlaybackService : IPlaybackService {
private val listeners = HashSet<() -> Unit>()
private val estimatedTime = EstimatedPosition()
override var playbackState = PlaybackState.Stopped
override var state = PlaybackState.Stopped
private set(value) {
field = value
}
override val currentTime: Double
get() {
return estimatedTime.get(track)
}
get() = estimatedTime.get(track)
override var repeatMode: RepeatMode = RepeatMode.None
private set(value) {
field = value
}
override var isShuffled: Boolean = false
override var shuffled: Boolean = false
private set(value) {
field = value
}
override var isMuted: Boolean = false
override var muted: Boolean = false
private set(value) {
field = value
}
@ -193,13 +186,13 @@ class RemotePlaybackService : IPlaybackService {
}
override fun pause() {
if (playbackState != PlaybackState.Paused) {
if (state != PlaybackState.Paused) {
pauseOrResume()
}
}
override fun resume() {
if (playbackState != PlaybackState.Playing) {
if (state != PlaybackState.Playing) {
pauseOrResume()
}
}
@ -296,10 +289,10 @@ class RemotePlaybackService : IPlaybackService {
get() = RemoteTrack(track)
private fun reset() {
playbackState = PlaybackState.Stopped
state = PlaybackState.Stopped
repeatMode = RepeatMode.None
isMuted = false
isShuffled = isMuted
muted = false
shuffled = muted
volume = 0.0
queuePosition = 0
queueCount = queuePosition
@ -328,9 +321,9 @@ class RemotePlaybackService : IPlaybackService {
throw IllegalArgumentException("invalid message!")
}
playbackState = PlaybackState.from(message.getStringOption(Key.STATE))
state = PlaybackState.from(message.getStringOption(Key.STATE))
when (playbackState) {
when (state) {
PlaybackState.Paused -> estimatedTime.pause()
PlaybackState.Playing -> {
estimatedTime.resume()
@ -340,8 +333,8 @@ class RemotePlaybackService : IPlaybackService {
}
repeatMode = RepeatMode.from(message.getStringOption(Key.REPEAT_MODE))
isShuffled = message.getBooleanOption(Key.SHUFFLED)
isMuted = message.getBooleanOption(Key.MUTED)
shuffled = message.getBooleanOption(Key.SHUFFLED)
muted = message.getBooleanOption(Key.MUTED)
volume = message.getDoubleOption(Key.VOLUME)
queueCount = message.getIntOption(Key.PLAY_QUEUE_COUNT)
queuePosition = message.getIntOption(Key.PLAY_QUEUE_POSITION)
@ -366,7 +359,7 @@ class RemotePlaybackService : IPlaybackService {
private fun scheduleTimeSyncMessage() {
handler.removeCallbacks(syncTimeRunnable)
if (playbackState == PlaybackState.Playing) {
if (state == PlaybackState.Playing) {
handler.postDelayed(syncTimeRunnable, SYNC_TIME_INTERVAL_MS)
}
}
@ -381,21 +374,10 @@ class RemotePlaybackService : IPlaybackService {
}
override val playlistQueryFactory: TrackListSlidingWindow.QueryFactory = object : TrackListSlidingWindow.QueryFactory() {
override fun count(): Observable<Int> {
return dataProvider.getPlayQueueTracksCount()
}
override fun all(): Observable<List<ITrack>>? {
return dataProvider.getPlayQueueTracks()
}
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> {
return dataProvider.getPlayQueueTracks(limit, offset)
}
override fun offline(): Boolean {
return false
}
override fun count(): Observable<Int> = dataProvider.getPlayQueueTracksCount()
override fun all(): Observable<List<ITrack>>? = dataProvider.getPlayQueueTracks()
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> = dataProvider.getPlayQueueTracks(limit, offset)
override fun offline(): Boolean = false
}
private val client = object : WebSocketService.Client {

View File

@ -10,20 +10,20 @@ import android.provider.Settings
import android.util.Log
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.model.impl.remote.RemoteTrack
import io.casey.musikcube.remote.injection.DaggerServiceComponent
import io.casey.musikcube.remote.injection.DataModule
import io.casey.musikcube.remote.model.impl.remote.RemoteTrack
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackState
import io.casey.musikcube.remote.service.playback.PlayerWrapper
import io.casey.musikcube.remote.service.playback.RepeatMode
import io.casey.musikcube.remote.service.system.SystemService
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.util.Strings
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import org.json.JSONObject
@ -195,7 +195,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
override fun pauseOrResume() {
if (playContext.currentPlayer != null) {
if (playbackState === PlaybackState.Playing || playbackState === PlaybackState.Buffering) {
if (state === PlaybackState.Playing || state === PlaybackState.Buffering) {
pause()
}
else {
@ -205,13 +205,13 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
override fun pause() {
if (playbackState != PlaybackState.Paused) {
if (state != PlaybackState.Paused) {
schedulePausedSleep()
killAudioFocus()
if (playContext.currentPlayer != null) {
playContext.currentPlayer?.pause()
setState(PlaybackState.Paused)
state = PlaybackState.Paused
}
}
}
@ -223,7 +223,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
if (playContext.currentPlayer != null) {
playContext.currentPlayer?.resume()
setState(PlaybackState.Playing)
state = PlaybackState.Playing
}
}
}
@ -233,7 +233,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
killAudioFocus()
playContext.stopPlaybackAndReset()
trackMetadataCache.clear()
setState(PlaybackState.Stopped)
state = PlaybackState.Stopped
}
override fun prev() {
@ -315,12 +315,12 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
return (playContext.currentPlayer?.position?.toDouble() ?: 0.0) / 1000.0
}
override var isShuffled: Boolean = false
override var shuffled: Boolean = false
private set(value) {
field = value
}
override var isMuted: Boolean = false
override var muted: Boolean = false
private set(value) {
field = value
}
@ -330,20 +330,24 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
field = value
}
override var playbackState = PlaybackState.Stopped
override var state = PlaybackState.Stopped
private set(value) {
field = value
if (field !== value) {
Log.d(TAG, "state = " + state)
field = value
notifyEventListeners()
}
}
override fun toggleShuffle() {
isShuffled = !isShuffled
shuffled = !shuffled
invalidateAndPrefetchNextTrackMetadata()
notifyEventListeners()
}
override fun toggleMute() {
isMuted = !isMuted
PlayerWrapper.setMute(isMuted)
muted = !muted
PlayerWrapper.setMute(muted)
notifyEventListeners()
}
@ -375,9 +379,9 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
private fun pauseTransient() {
if (playbackState !== PlaybackState.Paused) {
if (state !== PlaybackState.Paused) {
pausedByTransientLoss = true
setState(PlaybackState.Paused)
state = PlaybackState.Paused
playContext.currentPlayer?.pause()
}
}
@ -391,7 +395,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
private fun adjustVolume(delta: Float) {
if (isMuted) {
if (muted) {
toggleMute()
}
@ -460,22 +464,22 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
}
private val onCurrentPlayerStateChanged = { _: PlayerWrapper, state: PlayerWrapper.State ->
when (state) {
private val onCurrentPlayerStateChanged = { _: PlayerWrapper, newState: PlayerWrapper.State ->
when (newState) {
PlayerWrapper.State.Playing -> {
setState(PlaybackState.Playing)
state = PlaybackState.Playing
prefetchNextTrackAudio()
cancelScheduledPausedSleep()
precacheTrackMetadata(playContext.currentIndex, PRECACHE_METADATA_SIZE)
}
PlayerWrapper.State.Buffering -> setState(PlaybackState.Buffering)
PlayerWrapper.State.Buffering -> state = PlaybackState.Buffering
PlayerWrapper.State.Paused -> pause()
PlayerWrapper.State.Error -> pause()
PlayerWrapper.State.Finished -> if (playbackState !== PlaybackState.Paused) {
PlayerWrapper.State.Finished -> if (this.state !== PlaybackState.Paused) {
moveToNextTrack(false)
}
@ -491,14 +495,6 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
}
private fun setState(state: PlaybackState) {
if (playbackState !== state) {
Log.d(TAG, "state = " + state)
playbackState = state
notifyEventListeners()
}
}
@Synchronized private fun notifyEventListeners() {
for (listener in listeners) {
listener()
@ -556,7 +552,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
private fun resolveNextIndex(currentIndex: Int, count: Int, userInitiated: Boolean): Int {
if (isShuffled) { /* our shuffle matches actually random for now. */
if (shuffled) { /* our shuffle matches actually random for now. */
if (count <= 0) {
return currentIndex
}
@ -684,7 +680,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
}
private fun loadQueueAndPlay(newParams: QueueParams, startIndex: Int) {
setState(PlaybackState.Buffering)
state = PlaybackState.Buffering
cancelScheduledPausedSleep()
SystemService.wakeup()
@ -715,7 +711,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
},
{ error ->
Log.e(TAG, "failed to load track to play!", error)
setState(PlaybackState.Stopped)
state = PlaybackState.Stopped
},
{
if (this.params === newParams && playContext === newPlayContext) {
@ -845,7 +841,7 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
pause()
}
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> when (playbackState) {
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> when (state) {
PlaybackState.Playing,
PlaybackState.Buffering -> pauseTransient()
else -> { }

View File

@ -185,7 +185,7 @@ class SystemService : Service() {
var playing: ITrack? = null
if (playback != null) {
when (playback?.playbackState) {
when (playback?.state) {
PlaybackState.Playing -> mediaSessionState = PlaybackStateCompat.STATE_PLAYING
PlaybackState.Buffering -> mediaSessionState = PlaybackStateCompat.STATE_BUFFERING
PlaybackState.Paused -> mediaSessionState = PlaybackStateCompat.STATE_PAUSED

View File

@ -88,6 +88,8 @@ class Messages {
val PLAYING_CURRENT_TIME = "playing_current_time"
val PLAYLIST_ID = "playlist_id"
val PLAYLIST_NAME = "playlist_name"
val PREDICATE_CATEGORY = "predicate_category"
val PREDICATE_ID = "predicate_id"
val SUBQUERY = "subquery"
val TYPE = "type"
val OPTIONS = "options"

View File

@ -31,13 +31,14 @@ interface IDataProvider {
fun getPlaylists(): Observable<List<IPlaylist>>
fun getCategoryValues(type: String, filter: String = ""): Observable<List<ICategoryValue>>
fun getCategoryValues(type: String, predicateType: String = "", predicateId: Long = -1L, filter: String = ""): Observable<List<ICategoryValue>>
fun createPlaylist(playlistName: String, categoryType: String = "", categoryId: Long = -1, filter: String = ""): Observable<Long>
fun createPlaylist(playlistName: String, tracks: List<ITrack> = ArrayList()): Observable<Long>
fun createPlaylistWithExternalIds(playlistName: String, externalIds: List<String> = ArrayList()): Observable<Long>
fun appendToPlaylist(playlistId: Long, categoryType: String = "", categoryId: Long = -1, filter: String = "", offset: Long = -1): Observable<Boolean>
fun appendToPlaylist(playlistId: Long, tracks: List<ITrack> = ArrayList(), offset: Long = -1): Observable<Boolean>
fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable<Boolean>
fun appendToPlaylistWithExternalIds(playlistId: Long, externalIds: List<String> = ArrayList(), offset: Long = -1): Observable<Boolean>
fun renamePlaylist(playlistId: Long, newName: String): Observable<Boolean>
fun deletePlaylist(playlistId: Long): Observable<Boolean>

View File

@ -1,9 +1,9 @@
package io.casey.musikcube.remote.model.impl.remote
import io.casey.musikcube.remote.service.websocket.model.*
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.SocketMessage
import io.casey.musikcube.remote.service.websocket.WebSocketService
import io.casey.musikcube.remote.service.websocket.model.*
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
@ -189,10 +189,12 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
.observeOn(AndroidSchedulers.mainThread())
}
override fun getCategoryValues(type: String, filter: String): Observable<List<ICategoryValue>> {
override fun getCategoryValues(type: String, predicateType: String, predicateId: Long, filter: String): Observable<List<ICategoryValue>> {
val message = SocketMessage.Builder
.request(Messages.Request.QueryCategory)
.addOption(Messages.Key.CATEGORY, type)
.addOption(Messages.Key.PREDICATE_CATEGORY, predicateType)
.addOption(Messages.Key.PREDICATE_ID, predicateId)
.addOption(Messages.Key.FILTER, filter)
.build()
@ -293,6 +295,9 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
.observeOn(AndroidSchedulers.mainThread())
}
override fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable<Boolean> =
appendToPlaylist(playlistId, categoryValue.type, categoryValue.id)
override fun appendToPlaylistWithExternalIds(playlistId: Long, externalIds: List<String>, offset: Long): Observable<Boolean> {
val jsonArray = JSONArray()
externalIds.forEach { jsonArray.put(it) }

View File

@ -43,9 +43,7 @@ class RemoteTrack(val json: JSONObject) : ITrack {
return -1L
}
override fun toJson(): JSONObject {
return JSONObject(json.toString())
}
override fun toJson(): JSONObject = JSONObject(json.toString())
companion object {
private val CATEGORY_NAME_TO_ID: Map<String, String> = mapOf(

View File

@ -3,38 +3,44 @@ package io.casey.musikcube.remote.ui.albums.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
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.IAlbum
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.casey.musikcube.remote.util.Debouncer
import io.casey.musikcube.remote.ui.shared.constants.Navigation
import io.casey.musikcube.remote.util.Strings
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
import io.casey.musikcube.remote.ui.albums.adapter.AlbumBrowseAdapter
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.constants.Navigation
import io.casey.musikcube.remote.ui.shared.extension.*
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.casey.musikcube.remote.util.Strings
import io.reactivex.rxkotlin.subscribeBy
class AlbumBrowseActivity : BaseActivity(), Filterable {
private var adapter: Adapter = Adapter()
private var categoryName: String = ""
private var categoryId: Long = 0
private var lastFilter = ""
private lateinit var adapter: AlbumBrowseAdapter
private lateinit var playback: PlaybackMixin
private lateinit var data: DataProviderMixin
private lateinit var transport: TransportFragment
private lateinit var emptyView: EmptyListView
override fun onCreate(savedInstanceState: Bundle?) {
component.inject(this)
data = mixin(DataProviderMixin())
playback = mixin(PlaybackMixin())
mixin(ItemContextMenuMixin(this))
super.onCreate(savedInstanceState)
@ -46,6 +52,8 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
setTitleFromIntent(R.string.albums_title)
enableUpNavigation()
adapter = AlbumBrowseAdapter(eventListener, playback)
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
setupDefaultRecyclerView(recyclerView, adapter)
@ -86,8 +94,8 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
}
private fun initObservables() {
disposables.add(dataProvider.observeState().subscribe(
{ state ->
disposables.add(data.provider.observeState().subscribeBy(
onNext = { state ->
if (state.first == IDataProvider.State.Connected) {
filterDebouncer.call()
requery()
@ -95,15 +103,20 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
else {
emptyView.update(state.first, adapter.itemCount)
}
}, { /* error */ }))
},
onError = {
}))
}
private fun requery() {
dataProvider.getAlbumsForCategory(categoryName, categoryId, lastFilter)
.subscribe({ albumList ->
adapter.setModel(albumList)
emptyView.update(dataProvider.state, adapter.itemCount)
}, { /* error*/ })
data.provider.getAlbumsForCategory(categoryName, categoryId, lastFilter)
.subscribeBy(
onNext = { albumList ->
adapter.setModel(albumList)
emptyView.update(data.provider.state, adapter.itemCount)
},
onError = {
})
}
private val filterDebouncer = object : Debouncer<String>(350) {
@ -114,61 +127,15 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
}
}
private val onItemClickListener = { view: View ->
val album = view.tag as IAlbum
val intent = TrackListActivity.getStartIntent(
private val eventListener = object: AlbumBrowseAdapter.EventListener {
override fun onItemClicked(album: IAlbum) {
val intent = TrackListActivity.getStartIntent(
this@AlbumBrowseActivity, Messages.Category.ALBUM, album.id, album.value)
startActivityForResult(intent, Navigation.RequestCode.ALBUM_TRACKS_ACTIVITY)
}
startActivityForResult(intent, Navigation.RequestCode.ALBUM_TRACKS_ACTIVITY) }
private inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val title = itemView.findViewById<TextView>(R.id.title)
private val subtitle = itemView.findViewById<TextView>(R.id.subtitle)
internal fun bind(album: IAlbum) {
val playing = transport.playbackService!!.playingTrack
val playingId = playing.albumId
var titleColor = R.color.theme_foreground
var subtitleColor = R.color.theme_disabled_foreground
if (playingId != -1L && album.id == playingId) {
titleColor = R.color.theme_green
subtitleColor = R.color.theme_yellow
}
title.text = fallback(album.value, "-")
title.setTextColor(getColorCompat(titleColor))
subtitle.text = fallback(album.albumArtist, "-")
subtitle.setTextColor(getColorCompat(subtitleColor))
itemView.tag = album
}
}
private inner class Adapter : RecyclerView.Adapter<ViewHolder>() {
private var model: List<IAlbum> = listOf()
internal fun setModel(model: List<IAlbum>) {
this.model = model
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
view.setOnClickListener(onItemClickListener)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(model[position])
}
override fun getItemCount(): Int {
return model.size
override fun onActionClicked(view: View, album: IAlbum) {
mixin(ItemContextMenuMixin::class.java)?.showForCategory(album, view)
}
}
@ -176,9 +143,8 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
private val EXTRA_CATEGORY_NAME = "extra_category_name"
private val EXTRA_CATEGORY_ID = "extra_category_id"
fun getStartIntent(context: Context): Intent {
return Intent(context, AlbumBrowseActivity::class.java)
}
fun getStartIntent(context: Context): Intent =
Intent(context, AlbumBrowseActivity::class.java)
fun getStartIntent(context: Context, categoryName: String, categoryId: Long): Intent {
return Intent(context, AlbumBrowseActivity::class.java)
@ -198,8 +164,7 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
return intent
}
fun getStartIntent(context: Context, categoryName: String, categoryValue: ICategoryValue): Intent {
return getStartIntent(context, categoryName, categoryValue.id, categoryValue.value)
}
fun getStartIntent(context: Context, categoryName: String, categoryValue: ICategoryValue): Intent =
getStartIntent(context, categoryName, categoryValue.id, categoryValue.value)
}
}

View File

@ -0,0 +1,81 @@
package io.casey.musikcube.remote.ui.albums.adapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.injection.GlideApp
import io.casey.musikcube.remote.service.websocket.model.IAlbum
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.albumart.Size
import io.casey.musikcube.remote.ui.shared.model.albumart.getUrl
class AlbumBrowseAdapter(private val listener: EventListener,
private val playback: PlaybackMixin)
: RecyclerView.Adapter<AlbumBrowseAdapter.ViewHolder>()
{
interface EventListener {
fun onItemClicked(album: IAlbum)
fun onActionClicked(view: View, album: IAlbum)
}
private var model: List<IAlbum> = listOf()
internal fun setModel(model: List<IAlbum>) {
this.model = model
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
val action = view.findViewById<View>(R.id.action)
view.setOnClickListener({ v -> listener.onItemClicked(v.tag as IAlbum) })
action.setOnClickListener({ v -> listener.onActionClicked(v, v.tag as IAlbum) })
return ViewHolder(view, playback)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(model[position])
}
override fun getItemCount(): Int = model.size
inner class ViewHolder internal constructor(
itemView: View, playback: PlaybackMixin) : RecyclerView.ViewHolder(itemView) {
private val title = itemView.findViewById<TextView>(R.id.title)
private val subtitle = itemView.findViewById<TextView>(R.id.subtitle)
private val artwork = itemView.findViewById<ImageView>(R.id.artwork)
private val action = itemView.findViewById<View>(R.id.action)
internal fun bind(album: IAlbum) {
val playing = playback.service.playingTrack
val playingId = playing.albumId
var titleColor = R.color.theme_foreground
var subtitleColor = R.color.theme_disabled_foreground
if (playingId != -1L && album.id == playingId) {
titleColor = R.color.theme_green
subtitleColor = R.color.theme_yellow
}
artwork.visibility = View.VISIBLE
GlideApp.with(itemView.context).load(getUrl(album, Size.Large)).into(artwork)
title.text = fallback(album.value, "-")
title.setTextColor(getColorCompat(titleColor))
subtitle.text = fallback(album.albumArtist, "-")
subtitle.setTextColor(getColorCompat(subtitleColor))
itemView.tag = album
action.tag = album
}
}
}

View File

@ -3,51 +3,65 @@ package io.casey.musikcube.remote.ui.category.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
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.shared.extension.*
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.casey.musikcube.remote.util.Debouncer
import io.casey.musikcube.remote.ui.shared.constants.Navigation
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
import io.casey.musikcube.remote.ui.category.adapter.CategoryBrowseAdapter
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.constants.Navigation
import io.casey.musikcube.remote.ui.shared.extension.addTransportFragment
import io.casey.musikcube.remote.ui.shared.extension.enableUpNavigation
import io.casey.musikcube.remote.ui.shared.extension.initSearchMenu
import io.casey.musikcube.remote.ui.shared.extension.setupDefaultRecyclerView
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 io.casey.musikcube.remote.service.websocket.WebSocketService.State as SocketState
class CategoryBrowseActivity : BaseActivity(), Filterable {
interface DeepLink {
enum class NavigationType {
Tracks, Albums, Select;
companion object {
val TRACKS = 0
val ALBUMS = 1
fun get(ordinal: Int) = values()[ordinal]
}
}
private var adapter: Adapter = Adapter()
private var deepLinkType: Int = 0
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 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))
super.onCreate(savedInstanceState)
category = intent.getStringExtra(EXTRA_CATEGORY)
deepLinkType = intent.getIntExtra(EXTRA_DEEP_LINK_TYPE, DeepLink.ALBUMS)
adapter = Adapter()
predicateType = intent.getStringExtra(EXTRA_PREDICATE_TYPE) ?: ""
predicateId = intent.getLongExtra(EXTRA_PREDICATE_ID, -1)
navigationType = NavigationType.get(intent.getIntExtra(EXTRA_NAVIGATION_TYPE, NavigationType.Albums.ordinal))
adapter = CategoryBrowseAdapter(eventListener, playback, category)
setContentView(R.layout.recycler_view_activity)
setTitle(categoryTitleStringId)
@ -83,12 +97,12 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Navigation.ResponseCode.PLAYBACK_STARTED) {
setResult(Navigation.ResponseCode.PLAYBACK_STARTED)
finish()
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun setFilter(filter: String) {
@ -97,8 +111,8 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
}
private fun initObservers() {
disposables.add(dataProvider.observeState().subscribe(
{ states ->
disposables.add(data.provider.observeState().subscribeBy(
onNext = { states ->
when (states.first) {
IDataProvider.State.Connected -> {
filterDebouncer.cancel()
@ -109,25 +123,22 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
}
else -> { }
}
}, { /* error */ }
))
},
onError = {
}))
}
private val categoryTypeStringId: Int
get() {
return CATEGORY_NAME_TO_EMPTY_TYPE[category] ?: R.string.unknown_value
}
get() = CATEGORY_NAME_TO_EMPTY_TYPE[category] ?: R.string.unknown_value
private val categoryTitleStringId: Int
get() {
return CATEGORY_NAME_TO_TITLE[category] ?: R.string.unknown_value
}
get() = CATEGORY_NAME_TO_TITLE[category] ?: R.string.unknown_value
private fun requery() {
dataProvider.getCategoryValues(category, lastFilter ?: "").subscribe(
{ values -> adapter.setModel(values) },
{ /* error */ },
{ emptyView.update(dataProvider.state, adapter.itemCount)})
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) {
@ -138,25 +149,24 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
}
}
private val onItemClickListener = { view: View ->
val entry = view.tag as ICategoryValue
if (deepLinkType == DeepLink.ALBUMS) {
navigateToAlbums(entry)
private val eventListener = object: CategoryBrowseAdapter.EventListener {
override fun onItemClicked(value: ICategoryValue) {
when (navigationType) {
NavigationType.Albums -> navigateToAlbums(value)
NavigationType.Tracks -> navigateToTracks(value)
NavigationType.Select -> {
val intent = Intent()
.putExtra(EXTRA_CATEGORY, value.type)
.putExtra(EXTRA_ID, value.id)
setResult(RESULT_OK, intent)
finish()
}
}
}
else {
navigateToTracks(entry)
}
}
private val onItemLongClickListener = { view: View ->
/* if we deep link to albums by default, long press will get to
tracks. if we deep link to tracks, just ignore */
var result = false
if (deepLinkType == DeepLink.ALBUMS) {
navigateToTracks(view.tag as ICategoryValue)
result = true
override fun onActionClicked(view: View, value: ICategoryValue) {
mixin(ItemContextMenuMixin::class.java)?.showForCategory(value, view)
}
result
}
private fun navigateToAlbums(entry: ICategoryValue) {
@ -171,56 +181,12 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
startActivityForResult(intent, Navigation.RequestCode.CATEGORY_TRACKS_ACTIVITY)
}
private inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.title)
init {
itemView.findViewById<View>(R.id.subtitle).visibility = View.GONE
}
internal fun bind(categoryValue: ICategoryValue) {
val playing = transport.playbackService?.playingTrack
val playingId = playing?.getCategoryId(category) ?: -1
var titleColor = R.color.theme_foreground
if (playingId != -1L && categoryValue.id == playingId) {
titleColor = R.color.theme_green
}
title.text = fallback(categoryValue.value, getString(R.string.unknown_value))
title.setTextColor(getColorCompat(titleColor))
itemView.tag = categoryValue
}
}
private inner class Adapter : RecyclerView.Adapter<ViewHolder>() {
private var model: List<ICategoryValue> = ArrayList()
internal fun setModel(model: List<ICategoryValue>?) {
this.model = model ?: ArrayList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
view.setOnClickListener(onItemClickListener)
view.setOnLongClickListener(onItemLongClickListener)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(model[position])
}
override fun getItemCount(): Int {
return model.size
}
}
companion object {
private val EXTRA_CATEGORY = "extra_category"
private val EXTRA_DEEP_LINK_TYPE = "extra_deep_link_type"
val EXTRA_CATEGORY = "extra_category"
val EXTRA_ID = "extra_id"
private val EXTRA_PREDICATE_TYPE = "extra_predicate_type"
private val EXTRA_PREDICATE_ID = "extra_predicate_id"
private val EXTRA_NAVIGATION_TYPE = "extra_navigation_type"
private val CATEGORY_NAME_TO_TITLE: Map<String, Int> = mapOf(
Messages.Category.ALBUM_ARTIST to R.string.artists_title,
@ -236,15 +202,17 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
Messages.Category.ALBUM to R.string.browse_type_albums,
Messages.Category.PLAYLISTS to R.string.browse_type_playlists)
fun getStartIntent(context: Context, category: String): Intent {
fun getStartIntent(context: Context, category: String, predicateType: String = "", predicateId: Long = -1): Intent {
return Intent(context, CategoryBrowseActivity::class.java)
.putExtra(EXTRA_CATEGORY, category)
.putExtra(EXTRA_PREDICATE_TYPE, predicateType)
.putExtra(EXTRA_PREDICATE_ID, predicateId)
}
fun getStartIntent(context: Context, category: String, deepLinkType: Int): Intent {
fun getStartIntent(context: Context, category: String, navigationType: NavigationType): Intent {
return Intent(context, CategoryBrowseActivity::class.java)
.putExtra(EXTRA_CATEGORY, category)
.putExtra(EXTRA_DEEP_LINK_TYPE, deepLinkType)
.putExtra(EXTRA_NAVIGATION_TYPE, navigationType.ordinal)
}
}
}

View File

@ -0,0 +1,76 @@
package io.casey.musikcube.remote.ui.category.adapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
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.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
class CategoryBrowseAdapter(private val listener: EventListener,
private val playback: PlaybackMixin,
private val category: String)
: RecyclerView.Adapter<CategoryBrowseAdapter.ViewHolder>()
{
interface EventListener {
fun onItemClicked(value: ICategoryValue)
fun onActionClicked(view: View, value: ICategoryValue)
}
private var model: List<ICategoryValue> = ArrayList()
internal fun setModel(model: List<ICategoryValue>?) {
this.model = model ?: ArrayList()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
val action = view.findViewById<View>(R.id.action)
view.setOnClickListener({ v -> listener.onItemClicked(v.tag as ICategoryValue) })
action.setOnClickListener({ v -> listener.onActionClicked(v, v.tag as ICategoryValue) })
return ViewHolder(view, playback, category)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(model[position])
}
override fun getItemCount(): Int = model.size
class ViewHolder internal constructor(
itemView: View,
private val playback: PlaybackMixin,
private val category: String) : RecyclerView.ViewHolder(itemView)
{
private val title: TextView = itemView.findViewById(R.id.title)
private val action: View = itemView.findViewById(R.id.action)
init {
itemView.findViewById<View>(R.id.subtitle).visibility = View.GONE
}
internal fun bind(categoryValue: ICategoryValue) {
action.tag = categoryValue
action.visibility = if (category == Messages.Category.PLAYLISTS) View.GONE else View.VISIBLE
val playing = playback.service.playingTrack
val playingId = playing.getCategoryId(category)
var titleColor = R.color.theme_foreground
if (playingId > 0 && categoryValue.id == playingId) {
titleColor = R.color.theme_green
}
title.text = fallback(categoryValue.value, R.string.unknown_value)
title.setTextColor(getColorCompat(titleColor))
itemView.tag = categoryValue
}
}
}

View File

@ -18,36 +18,38 @@ import android.widget.CompoundButton
import android.widget.SeekBar
import android.widget.TextView
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackState
import io.casey.musikcube.remote.service.playback.RepeatMode
import io.casey.musikcube.remote.ui.category.activity.*
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.WebSocketService
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.activity.CategoryBrowseActivity
import io.casey.musikcube.remote.ui.home.fragment.InvalidPasswordDialogFragment
import io.casey.musikcube.remote.ui.home.view.MainMetadataView
import io.casey.musikcube.remote.ui.playqueue.activity.PlayQueueActivity
import io.casey.musikcube.remote.ui.settings.activity.SettingsActivity
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.extension.setCheckWithoutEvent
import io.casey.musikcube.remote.ui.shared.extension.showSnackbar
import io.casey.musikcube.remote.ui.home.fragment.InvalidPasswordDialogFragment
import io.casey.musikcube.remote.ui.shared.util.UpdateCheck
import io.casey.musikcube.remote.ui.home.view.MainMetadataView
import io.casey.musikcube.remote.ui.shared.util.Duration
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.service.websocket.WebSocketService
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
import io.casey.musikcube.remote.ui.playqueue.activity.PlayQueueActivity
import io.casey.musikcube.remote.ui.settings.activity.SettingsActivity
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.util.UpdateCheck
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
class MainActivity : BaseActivity() {
private val handler = Handler()
private lateinit var prefs: SharedPreferences
private var playback: IPlaybackService? = null
private var updateCheck: UpdateCheck = UpdateCheck()
private var seekbarValue = -1
private var blink = 0
private lateinit var prefs: SharedPreferences
private lateinit var data: DataProviderMixin
private lateinit var playback: PlaybackMixin
/* views */
private lateinit var mainLayout: View
private lateinit var metadataView: MainMetadataView
@ -67,17 +69,18 @@ class MainActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
component.inject(this)
data = mixin(DataProviderMixin())
playback = mixin(PlaybackMixin({ rebindUi() }))
super.onCreate(savedInstanceState)
prefs = this.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
playback = playbackService
setContentView(R.layout.activity_main)
bindEventListeners()
if (!socketService.hasValidConnection()) {
if (!data.wss.hasValidConnection()) {
startActivity(SettingsActivity.getStartIntent(this))
}
}
@ -91,7 +94,6 @@ class MainActivity : BaseActivity() {
override fun onResume() {
super.onResume()
playback = playbackService
metadataView.onResume()
bindCheckBoxEventListeners()
rebindUi()
@ -106,7 +108,7 @@ class MainActivity : BaseActivity() {
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
val connected = socketService.state === WebSocketService.State.Connected
val connected = data.wss.state === WebSocketService.State.Connected
val streaming = isStreamingSelected
menu.findItem(R.id.action_playlists).isEnabled = connected
@ -137,7 +139,7 @@ class MainActivity : BaseActivity() {
R.id.action_playlists -> {
startActivity(CategoryBrowseActivity.getStartIntent(
this, Messages.Category.PLAYLISTS, CategoryBrowseActivity.DeepLink.TRACKS))
this, Messages.Category.PLAYLISTS, CategoryBrowseActivity.NavigationType.Tracks))
return true
}
@ -150,11 +152,8 @@ class MainActivity : BaseActivity() {
return super.onOptionsItemSelected(item)
}
override val playbackServiceEventListener: (() -> Unit)?
get() = playbackEvents
private fun initObservers() {
disposables.add(dataProvider.observeState().subscribe(
disposables.add(data.provider.observeState().subscribe(
{ states ->
when (states.first) {
IDataProvider.State.Connected -> rebindUi()
@ -163,7 +162,7 @@ class MainActivity : BaseActivity() {
}
}, { /* error */ }))
disposables.add(dataProvider.observeAuthFailure().subscribe(
disposables.add(data.provider.observeAuthFailure().subscribe(
{
val tag = InvalidPasswordDialogFragment.TAG
if (supportFragmentManager.findFragmentByTag(tag) == null) {
@ -198,7 +197,7 @@ class MainActivity : BaseActivity() {
val streaming = isStreamingSelected
if (streaming) {
playback?.stop()
playback.service.stop()
}
prefs.edit().putBoolean(Prefs.Key.STREAMING_PLAYBACK, !streaming)?.apply()
@ -210,8 +209,7 @@ class MainActivity : BaseActivity() {
showSnackbar(mainLayout, messageId)
reloadPlaybackService()
playback = playbackService
playback.reload()
invalidateOptionsMenu()
rebindUi()
@ -247,20 +245,20 @@ class MainActivity : BaseActivity() {
totalTime = findViewById(R.id.total_time)
seekbar = findViewById(R.id.seekbar)
findViewById<View>(R.id.button_prev).setOnClickListener { _: View -> playback?.prev() }
findViewById<View>(R.id.button_prev).setOnClickListener { _: View -> playback.service.prev() }
findViewById<View>(R.id.button_play_pause).setOnClickListener { _: View ->
if (playback?.playbackState === PlaybackState.Stopped) {
playback?.playAll()
if (playback.service.state === PlaybackState.Stopped) {
playback.service.playAll()
}
else {
playback?.pauseOrResume()
playback.service.pauseOrResume()
}
}
findViewById<View>(R.id.button_next).setOnClickListener { _: View -> playback?.next() }
findViewById<View>(R.id.button_next).setOnClickListener { _: View -> playback.service.next() }
disconnectedButton.setOnClickListener { _ -> socketService.reconnect() }
disconnectedButton.setOnClickListener { _ -> data.wss.reconnect() }
seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
@ -275,7 +273,7 @@ class MainActivity : BaseActivity() {
override fun onStopTrackingTouch(seekBar: SeekBar) {
if (seekbarValue != -1) {
playback?.seekTo(seekbarValue.toDouble())
playback.service.seekTo(seekbarValue.toDouble())
seekbarValue = -1
}
}
@ -297,7 +295,7 @@ class MainActivity : BaseActivity() {
findViewById<View>(R.id.button_play_queue).setOnClickListener { _ -> navigateToPlayQueue() }
findViewById<View>(R.id.metadata_container).setOnClickListener { _ ->
if (playback?.queueCount ?: 0 > 0) {
if (playback.service.queueCount > 0) {
navigateToPlayQueue()
}
}
@ -310,17 +308,13 @@ class MainActivity : BaseActivity() {
}
private fun rebindUi() {
if (playback == null) {
throw IllegalStateException()
}
val playbackState = playback?.playbackState
val playbackState = playback.service.state
val streaming = prefs.getBoolean(Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK)
val connected = socketService.state === WebSocketService.State.Connected
val connected = data.wss.state === WebSocketService.State.Connected
val stopped = playbackState === PlaybackState.Stopped
val playing = playbackState === PlaybackState.Playing
val buffering = playbackState === PlaybackState.Buffering
val showMetadataView = !stopped && (playback?.queueCount ?: 0) > 0
val showMetadataView = !stopped && (playback.service.queueCount) > 0
/* bottom section: transport controls */
playPause.setText(if (playing || buffering) R.string.button_pause else R.string.button_play)
@ -328,14 +322,14 @@ class MainActivity : BaseActivity() {
connectedNotPlayingContainer.visibility = if (connected && stopped) View.VISIBLE else View.GONE
disconnectedOverlay.visibility = if (connected || !stopped) View.GONE else View.VISIBLE
val repeatMode = playback?.repeatMode
val repeatMode = playback.service.repeatMode
val repeatChecked = repeatMode !== RepeatMode.None
repeatCb.text = getString(REPEAT_TO_STRING_ID[repeatMode] ?: R.string.unknown_value)
repeatCb.setCheckWithoutEvent(repeatChecked, this.repeatListener)
shuffleCb.text = getString(if (streaming) R.string.button_random else R.string.button_shuffle)
shuffleCb.setCheckWithoutEvent(playback?.isShuffled ?: false, shuffleListener)
muteCb.setCheckWithoutEvent(playback?.isMuted ?: false, muteListener)
shuffleCb.setCheckWithoutEvent(playback.service.shuffled, shuffleListener)
muteCb.setCheckWithoutEvent(playback.service.muted, muteListener)
/* middle section: connected, disconnected, and metadata views */
connectedNotPlayingContainer.visibility = View.GONE
@ -362,7 +356,7 @@ class MainActivity : BaseActivity() {
}
private fun navigateToPlayQueue() {
startActivity(PlayQueueActivity.getStartIntent(this@MainActivity, playback?.queuePosition ?: 0))
startActivity(PlayQueueActivity.getStartIntent(this@MainActivity, playback.service.queuePosition ?: 0))
}
private fun scheduleUpdateTime(immediate: Boolean) {
@ -372,17 +366,17 @@ class MainActivity : BaseActivity() {
private val updateTimeRunnable = object: Runnable {
override fun run() {
val duration = playback?.duration ?: 0.0
val current: Double = if (seekbarValue == -1) playback?.currentTime ?: 0.0 else seekbarValue.toDouble()
val duration = playback.service.duration
val current: Double = if (seekbarValue == -1) playback.service.currentTime else seekbarValue.toDouble()
currentTime.text = Duration.format(current)
totalTime.text = Duration.format(duration)
seekbar.max = duration.toInt()
seekbar.progress = current.toInt()
seekbar.secondaryProgress = playback?.bufferedTime?.toInt() ?: 0
seekbar.secondaryProgress = playback.service.bufferedTime.toInt()
var currentTimeColor = R.color.theme_foreground
if (playback?.playbackState === PlaybackState.Paused) {
if (playback.service.state === PlaybackState.Paused) {
currentTimeColor =
if (++blink % 2 == 0) R.color.theme_foreground
else R.color.theme_blink_foreground
@ -395,19 +389,19 @@ class MainActivity : BaseActivity() {
}
private val muteListener = { _: CompoundButton, b: Boolean ->
if (b != playback?.isMuted) {
playback?.toggleMute()
if (b != playback.service.muted) {
playback.service.toggleMute()
}
}
private val shuffleListener = { _: CompoundButton, b: Boolean ->
if (b != playback?.isShuffled) {
playback?.toggleShuffle()
if (b != playback.service.shuffled) {
playback.service.toggleShuffle()
}
}
private fun onRepeatListener() {
val currentMode = playback?.repeatMode
val currentMode = playback.service.repeatMode
var newMode = RepeatMode.None
@ -422,7 +416,7 @@ class MainActivity : BaseActivity() {
repeatCb.text = getString(REPEAT_TO_STRING_ID[newMode] ?: R.string.unknown_value)
repeatCb.setCheckWithoutEvent(checked, repeatListener)
playback?.toggleRepeatMode()
playback.service.toggleRepeatMode()
}
private fun runUpdateCheck() {
@ -446,8 +440,6 @@ class MainActivity : BaseActivity() {
onRepeatListener()
}
private val playbackEvents = { rebindUi() }
class UpdateAvailableDialog: DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = LayoutInflater.from(activity)
@ -525,10 +517,7 @@ class MainActivity : BaseActivity() {
companion object {
val TAG = "switch_to_offline_tracks_dialog"
fun newInstance(): SwitchToOfflineTracksDialog {
return SwitchToOfflineTracksDialog()
}
fun newInstance(): SwitchToOfflineTracksDialog = SwitchToOfflineTracksDialog()
}
}

View File

@ -109,7 +109,7 @@ class MainMetadataView : FrameLayout {
val playback = playbackService
val playing = playbackService.playingTrack
val buffering = playback.playbackState == PlaybackState.Buffering
val buffering = playback.state == PlaybackState.Buffering
val streaming = playback is StreamingPlaybackService
val artist = fallback(playing.artist, "")
@ -185,7 +185,7 @@ class MainMetadataView : FrameLayout {
private fun rebindAlbumArtistWithArtTextView(playback: IPlaybackService) {
val playing = playback.playingTrack
val buffering = playback.playbackState == PlaybackState.Buffering
val buffering = playback.state == PlaybackState.Buffering
val artist = fallback(
playing.artist,
@ -230,7 +230,7 @@ class MainMetadataView : FrameLayout {
}
private fun updateAlbumArt(albumArtUrl: String = "") {
if (playbackService.playbackState == PlaybackState.Stopped) {
if (playbackService.state == PlaybackState.Stopped) {
setMetadataDisplayMode(DisplayMode.NoArtwork)
}

View File

@ -12,26 +12,29 @@ import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.reactivex.rxkotlin.subscribeBy
class PlayQueueActivity : BaseActivity() {
private var adapter: Adapter = Adapter()
private var offlineQueue: Boolean = false
private var playback: IPlaybackService? = null
private lateinit var data: DataProviderMixin
private lateinit var playback: PlaybackMixin
private lateinit var tracks: TrackListSlidingWindow
private lateinit var emptyView: EmptyListView
override fun onCreate(savedInstanceState: Bundle?) {
component.inject(this)
data = mixin(DataProviderMixin())
playback = mixin(PlaybackMixin(playbackEvents))
super.onCreate(savedInstanceState)
playback = playbackService
setContentView(R.layout.recycler_view_activity)
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
@ -42,22 +45,24 @@ class PlayQueueActivity : BaseActivity() {
emptyView.emptyMessage = getString(R.string.play_queue_empty)
emptyView.alternateView = recyclerView
val queryFactory = playback!!.playlistQueryFactory
offlineQueue = playback!!.playlistQueryFactory.offline()
val queryFactory = playback.service.playlistQueryFactory
offlineQueue = playback.service.playlistQueryFactory.offline()
tracks = TrackListSlidingWindow(recyclerView, dataProvider, queryFactory)
tracks = TrackListSlidingWindow(recyclerView, data.provider, queryFactory)
tracks.setInitialPosition(intent.getIntExtra(EXTRA_PLAYING_INDEX, -1))
tracks.setOnMetadataLoadedListener(slidingWindowListener)
dataProvider.observeState().subscribe(
{ states ->
data.provider.observeState().subscribeBy(
onNext = { states ->
if (states.first == IDataProvider.State.Connected) {
tracks.requery()
}
else {
emptyView.update(states.first, adapter.itemCount)
}
}, { /* error */ })
},
onError = {
})
setTitleFromIntent(R.string.play_queue_title)
addTransportFragment()
@ -79,13 +84,9 @@ class PlayQueueActivity : BaseActivity() {
}
}
override val playbackServiceEventListener: (() -> Unit)?
get() = playbackEvents
private val onItemClickListener = View.OnClickListener { v ->
if (v.tag is Int) {
val index = v.tag as Int
playback?.playAt(index)
playback.service.playAt(v.tag as Int)
}
}
@ -112,7 +113,7 @@ class PlayQueueActivity : BaseActivity() {
subtitle.text = "-"
}
else {
val playing = playback!!.playingTrack
val playing = playback.service.playingTrack
val entryExternalId = track.externalId
val playingExternalId = playing.externalId
@ -142,14 +143,12 @@ class PlayQueueActivity : BaseActivity() {
holder.bind(tracks.getTrack(position), position)
}
override fun getItemCount(): Int {
return tracks.count
}
override fun getItemCount(): Int = tracks.count
}
private val slidingWindowListener = object : TrackListSlidingWindow.OnMetadataLoadedListener {
override fun onReloaded(count: Int) {
emptyView.update(dataProvider.state, count)
emptyView.update(data.provider.state, count)
}
override fun onMetadataLoaded(offset: Int, count: Int) {}

View File

@ -17,12 +17,13 @@ import com.uacf.taskrunner.Task
import com.uacf.taskrunner.Tasks
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.ui.settings.model.Connection
import io.casey.musikcube.remote.service.playback.PlayerWrapper
import io.casey.musikcube.remote.service.playback.impl.streaming.StreamProxy
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.ui.settings.model.Connection
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import java.util.*
import io.casey.musikcube.remote.ui.settings.constants.Prefs.Default as Defaults
import io.casey.musikcube.remote.ui.settings.constants.Prefs.Key as Keys
@ -40,8 +41,10 @@ class SettingsActivity : BaseActivity() {
private lateinit var bitrateSpinner: Spinner
private lateinit var cacheSpinner: Spinner
private lateinit var prefs: SharedPreferences
private lateinit var data: DataProviderMixin
override fun onCreate(savedInstanceState: Bundle?) {
data = mixin(DataProviderMixin())
component.inject(this)
super.onCreate(savedInstanceState)
prefs = this.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
@ -237,25 +240,25 @@ class SettingsActivity : BaseActivity() {
try {
prefs.edit()
.putString(Keys.ADDRESS, addr)
.putInt(Keys.MAIN_PORT, if (port.isNotEmpty()) port.toInt() else 0)
.putInt(Keys.AUDIO_PORT, if (httpPort.isNotEmpty()) httpPort.toInt() else 0)
.putString(Keys.PASSWORD, password)
.putBoolean(Keys.ALBUM_ART_ENABLED, albumArtCheckbox.isChecked)
.putBoolean(Keys.MESSAGE_COMPRESSION_ENABLED, messageCompressionCheckbox.isChecked)
.putBoolean(Keys.SOFTWARE_VOLUME, softwareVolume.isChecked)
.putBoolean(Keys.SSL_ENABLED, sslCheckbox.isChecked)
.putBoolean(Keys.CERT_VALIDATION_DISABLED, certCheckbox.isChecked)
.putInt(Keys.TRANSCODER_BITRATE_INDEX, bitrateSpinner.selectedItemPosition)
.putInt(Keys.DISK_CACHE_SIZE_INDEX, cacheSpinner.selectedItemPosition)
.apply()
.putString(Keys.ADDRESS, addr)
.putInt(Keys.MAIN_PORT, if (port.isNotEmpty()) port.toInt() else 0)
.putInt(Keys.AUDIO_PORT, if (httpPort.isNotEmpty()) httpPort.toInt() else 0)
.putString(Keys.PASSWORD, password)
.putBoolean(Keys.ALBUM_ART_ENABLED, albumArtCheckbox.isChecked)
.putBoolean(Keys.MESSAGE_COMPRESSION_ENABLED, messageCompressionCheckbox.isChecked)
.putBoolean(Keys.SOFTWARE_VOLUME, softwareVolume.isChecked)
.putBoolean(Keys.SSL_ENABLED, sslCheckbox.isChecked)
.putBoolean(Keys.CERT_VALIDATION_DISABLED, certCheckbox.isChecked)
.putInt(Keys.TRANSCODER_BITRATE_INDEX, bitrateSpinner.selectedItemPosition)
.putInt(Keys.DISK_CACHE_SIZE_INDEX, cacheSpinner.selectedItemPosition)
.apply()
if (!softwareVolume.isChecked) {
PlayerWrapper.setVolume(1.0f)
}
StreamProxy.reload()
wss.disconnect()
data.wss.disconnect()
finish()
}
@ -268,10 +271,10 @@ class SettingsActivity : BaseActivity() {
if (SaveAsTask.match(taskName)) {
if ((result as SaveAsTask.Result) == SaveAsTask.Result.Exists) {
val connection = (task as SaveAsTask).connection
if (!dialogVisible(ConfirmOverwiteDialog.TAG)) {
if (!dialogVisible(ConfirmOverwriteDialog.TAG)) {
showDialog(
ConfirmOverwiteDialog.newInstance(connection),
ConfirmOverwiteDialog.TAG)
ConfirmOverwriteDialog.newInstance(connection),
ConfirmOverwriteDialog.TAG)
}
}
else {
@ -364,7 +367,7 @@ class SettingsActivity : BaseActivity() {
}
}
class ConfirmOverwiteDialog : DialogFragment() {
class ConfirmOverwriteDialog : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dlg = AlertDialog.Builder(activity)
.setTitle(R.string.settings_confirm_overwrite_title)
@ -385,10 +388,10 @@ class SettingsActivity : BaseActivity() {
val TAG = "confirm_overwrite_dialog"
private val EXTRA_CONNECTION = "extra_connection"
fun newInstance(connection: Connection): ConfirmOverwiteDialog {
fun newInstance(connection: Connection): ConfirmOverwriteDialog {
val args = Bundle()
args.putParcelable(EXTRA_CONNECTION, connection)
val result = ConfirmOverwiteDialog()
val result = ConfirmOverwriteDialog()
result.arguments = args
return result
}

View File

@ -1,36 +1,34 @@
package io.casey.musikcube.remote.ui.shared.activity
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.media.AudioManager
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.KeyEvent
import android.view.MenuItem
import com.uacf.taskrunner.LifecycleDelegate
import com.uacf.taskrunner.Runner
import com.uacf.taskrunner.Task
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.framework.components.ComponentSet
import io.casey.musikcube.remote.framework.components.IComponent
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import io.casey.musikcube.remote.injection.*
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackServiceFactory
import io.casey.musikcube.remote.ui.shared.extension.hideKeyboard
import io.casey.musikcube.remote.framework.IMixin
import io.casey.musikcube.remote.framework.MixinSet
import io.casey.musikcube.remote.framework.ViewModel
import io.casey.musikcube.remote.injection.DaggerViewComponent
import io.casey.musikcube.remote.injection.DataModule
import io.casey.musikcube.remote.injection.ViewComponent
import io.casey.musikcube.remote.ui.settings.constants.Prefs
import io.casey.musikcube.remote.service.websocket.WebSocketService
import io.casey.musikcube.remote.ui.shared.extension.hideKeyboard
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.mixin.RunnerMixin
import io.casey.musikcube.remote.ui.shared.mixin.ViewModelMixin
import io.reactivex.disposables.CompositeDisposable
import javax.inject.Inject
abstract class BaseActivity : AppCompatActivity(), Runner.TaskCallbacks {
abstract class BaseActivity : AppCompatActivity(), ViewModel.Provider, Runner.TaskCallbacks {
protected var disposables = CompositeDisposable()
private lateinit var runnerDelegate: LifecycleDelegate
private lateinit var prefs: SharedPreferences
private var paused = false
private val components = ComponentSet()
@Inject lateinit var wss: WebSocketService
@Inject lateinit var dataProvider: IDataProvider
private val mixins = MixinSet()
protected val component: ViewComponent =
DaggerViewComponent.builder()
@ -40,91 +38,56 @@ abstract class BaseActivity : AppCompatActivity(), Runner.TaskCallbacks {
override fun onCreate(savedInstanceState: Bundle?) {
component.inject(this)
mixin(RunnerMixin(this, javaClass))
super.onCreate(savedInstanceState)
components.onCreate(savedInstanceState ?: Bundle())
volumeControlStream = AudioManager.STREAM_MUSIC
runnerDelegate = LifecycleDelegate(this, this, javaClass, null)
runnerDelegate.onCreate(savedInstanceState)
playbackService = PlaybackServiceFactory.instance(this)
prefs = getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
volumeControlStream = AudioManager.STREAM_MUSIC
mixins.onCreate(savedInstanceState ?: Bundle())
}
override fun onStart() {
super.onStart()
components.onStart()
mixins.onStart()
}
override fun onResume() {
super.onResume()
components.onResume()
dataProvider.attach()
runnerDelegate.onResume()
playbackService = PlaybackServiceFactory.instance(this)
val playbackListener = playbackServiceEventListener
if (playbackListener != null) {
this.playbackService?.connect(playbackServiceEventListener!!)
}
mixins.onResume()
paused = false
}
override fun onPause() {
hideKeyboard()
super.onPause()
components.onPause()
dataProvider.detach()
runnerDelegate.onPause()
val playbackListener = playbackServiceEventListener
if (playbackListener != null) {
playbackService?.disconnect(playbackServiceEventListener!!)
}
mixins.onPause()
disposables.dispose()
disposables = CompositeDisposable()
paused = true
}
override fun onStop() {
super.onStop()
components.onStop()
mixins.onStop()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mixins.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
components.onSaveInstanceState(outState)
runnerDelegate.onSaveInstanceState(outState)
mixins.onSaveInstanceState(outState)
}
override fun onDestroy() {
super.onDestroy()
components.onDestroy()
runnerDelegate.onDestroy()
dataProvider.destroy()
mixins.onDestroy()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
val streaming = prefs.getBoolean(
Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK)
/* if we're not streaming we want the hardware buttons to go out to the system */
if (!streaming) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
playbackService?.volumeDown()
return true
}
else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
playbackService?.volumeUp()
return true
}
if (mixin(PlaybackMixin::class.java)?.onKeyDown(keyCode) == true) {
return true
}
return super.onKeyDown(keyCode, event)
@ -140,41 +103,18 @@ abstract class BaseActivity : AppCompatActivity(), Runner.TaskCallbacks {
}
override fun onTaskCompleted(taskName: String, taskId: Long, task: Task<*, *>, result: Any) {
}
override fun onTaskError(s: String, l: Long, task: Task<*, *>, throwable: Throwable) {
}
protected fun isPaused(): Boolean = paused
protected fun component(component: IComponent) = components.add(component)
protected fun <T> component(cls: Class<out IComponent>): T? = components.get(cls)
protected val socketService: WebSocketService get() = wss
protected var playbackService: IPlaybackService? = null
private set
override fun <T: ViewModel<*>> createViewModel(): T? = null
protected fun <T: ViewModel<*>> getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get<T>() as T
protected fun <T: IMixin> mixin(mixin: T): T = mixins.add(mixin)
protected fun <T: IMixin> mixin(cls: Class<out T>): T? = mixins.get(cls)
protected val runner: Runner
get() = runnerDelegate.runner()
protected fun reloadPlaybackService() {
if (!isPaused() && playbackService != null) {
val playbackListener = playbackServiceEventListener
if (playbackListener != null) {
playbackService?.disconnect(playbackServiceEventListener!!)
}
playbackService = PlaybackServiceFactory.instance(this)
if (playbackListener != null) {
playbackService?.connect(playbackServiceEventListener!!)
}
}
}
protected open val playbackServiceEventListener: (() -> Unit)?
get() = null
get() = mixin(RunnerMixin::class.java)!!.runner
}

View File

@ -1,5 +1,6 @@
package io.casey.musikcube.remote.ui.shared.extension
import android.app.Activity
import android.app.SearchManager
import android.content.Context
import android.support.design.widget.Snackbar
@ -19,6 +20,7 @@ import android.widget.CheckBox
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.TextView
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.ui.shared.activity.Filterable
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
@ -39,6 +41,10 @@ fun AppCompatActivity.setupDefaultRecyclerView(
recyclerView.addItemDecoration(dividerItemDecoration)
}
fun RecyclerView.ViewHolder.getColorCompat(resourceId: Int): Int {
return ContextCompat.getColor(itemView.context, resourceId)
}
fun View.getColorCompat(resourceId: Int): Int {
return ContextCompat.getColor(context, resourceId)
}
@ -167,27 +173,37 @@ fun DialogFragment.hideKeyboard() {
hideKeyboard(activity, activity.findViewById(android.R.id.content))
}
fun AppCompatActivity.dialogVisible(tag: String): Boolean {
return this.supportFragmentManager.findFragmentByTag(tag) != null
}
fun AppCompatActivity.dialogVisible(tag: String): Boolean =
this.supportFragmentManager.findFragmentByTag(tag) != null
fun AppCompatActivity.showDialog(dialog: DialogFragment, tag: String) {
dialog.show(this.supportFragmentManager, tag)
}
fun AppCompatActivity.showSnackbar(view: View, stringId: Int) {
fun showSnackbar(view: View, stringId: Int, bgColor: Int, fgColor: Int) {
val sb = Snackbar.make(view, stringId, Snackbar.LENGTH_LONG)
val sbView = sb.view
sbView.setBackgroundColor(getColorCompat(R.color.color_primary))
val context = view.context
sbView.setBackgroundColor(ContextCompat.getColor(context, bgColor))
val tv = sbView.findViewById<TextView>(android.support.design.R.id.snackbar_text)
tv.setTextColor(getColorCompat(R.color.theme_foreground))
tv.setTextColor(ContextCompat.getColor(context, fgColor))
sb.show()
}
fun AppCompatActivity.showSnackbar(viewId: Int, stringId: Int) {
this.showSnackbar(this.findViewById<View>(viewId), stringId)
fun showSnackbar(view: View, stringId: Int) {
showSnackbar(view, stringId, R.color.color_primary, R.color.theme_foreground)
}
fun fallback(input: String?, fallback: String): String {
return if (input.isNullOrEmpty()) fallback else input!!
}
fun showErrorSnackbar(view: View, stringId: Int) {
showSnackbar(view, stringId, R.color.theme_red, R.color.theme_foreground)
}
fun AppCompatActivity.showSnackbar(viewId: Int, stringId: Int) {
showSnackbar(this.findViewById<View>(viewId), stringId)
}
fun fallback(input: String?, fallback: String): String =
if (input.isNullOrEmpty()) fallback else input!!
fun fallback(input: String?, fallback: Int): String =
if (input.isNullOrEmpty()) Application.Companion.instance!!.getString(fallback) else input!!

View File

@ -0,0 +1,59 @@
package io.casey.musikcube.remote.ui.shared.fragment
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import io.casey.musikcube.remote.framework.IMixin
import io.casey.musikcube.remote.framework.MixinSet
import io.casey.musikcube.remote.framework.ViewModel
import io.casey.musikcube.remote.ui.shared.mixin.ViewModelMixin
open class BaseFragment: Fragment(), ViewModel.Provider {
private val mixins = MixinSet()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mixins.onCreate(savedInstanceState ?: Bundle())
}
override fun onStart() {
super.onStart()
mixins.onStart()
}
override fun onResume() {
super.onResume()
mixins.onResume()
}
override fun onPause() {
super.onPause()
mixins.onPause()
}
override fun onStop() {
super.onStop()
mixins.onStop()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mixins.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
mixins.onSaveInstanceState(outState ?: Bundle())
}
override fun onDestroy() {
super.onDestroy()
mixins.onDestroy()
}
override fun <T: ViewModel<*>> createViewModel(): T? = null
protected fun <T: ViewModel<*>> getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get<T>() as T
protected fun <T: IMixin> mixin(mixin: T): T = mixins.add(mixin)
protected fun <T: IMixin> mixin(cls: Class<out T>): T? = mixins.get(cls)
}

View File

@ -2,34 +2,33 @@ package io.casey.musikcube.remote.ui.shared.fragment
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.casey.musikcube.remote.ui.home.activity.MainActivity
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackServiceFactory
import io.casey.musikcube.remote.service.playback.PlaybackState
import io.casey.musikcube.remote.ui.home.activity.MainActivity
import io.casey.musikcube.remote.ui.playqueue.activity.PlayQueueActivity
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
class TransportFragment : Fragment() {
private var rootView: View? = null
private var buffering: View? = null
private var title: TextView? = null
private var playPause: TextView? = null
class TransportFragment: BaseFragment() {
private lateinit var rootView: View
private lateinit var buffering: View
private lateinit var title: TextView
private lateinit var playPause: TextView
lateinit var playback: PlaybackMixin
private set
interface OnModelChangedListener {
fun onChanged(fragment: TransportFragment)
}
override fun onCreateView(inflater: LayoutInflater?,
container: ViewGroup?,
savedInstanceState: Bundle?): View?
override fun onCreateView(
inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View?
{
this.rootView = inflater!!.inflate(R.layout.fragment_transport, container, false)
bindEventHandlers()
@ -38,83 +37,71 @@ class TransportFragment : Fragment() {
}
override fun onCreate(savedInstanceState: Bundle?) {
playback = mixin(PlaybackMixin(playbackListener))
super.onCreate(savedInstanceState)
this.playbackService = PlaybackServiceFactory.instance(activity)
}
override fun onPause() {
super.onPause()
this.playbackService?.disconnect(playbackListener)
}
override fun onResume() {
super.onResume()
rebindUi()
this.playbackService?.connect(playbackListener)
}
var playbackService: IPlaybackService? = null
private set
var modelChangedListener: OnModelChangedListener? = null
set(value) {
field = value
}
private fun bindEventHandlers() {
this.title = this.rootView?.findViewById<TextView>(R.id.track_title)
this.buffering = this.rootView?.findViewById<View>(R.id.buffering)
this.title = this.rootView.findViewById(R.id.track_title)
this.buffering = this.rootView.findViewById(R.id.buffering)
val titleBar = this.rootView?.findViewById<View>(R.id.title_bar)
val titleBar = this.rootView.findViewById<View>(R.id.title_bar)
titleBar?.setOnClickListener { _: View ->
if (playbackService?.playbackState != PlaybackState.Stopped) {
if (playback.service.state != PlaybackState.Stopped) {
val intent = PlayQueueActivity
.getStartIntent(activity, playbackService?.queuePosition ?: 0)
.getStartIntent(activity, playback.service.queuePosition)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
}
}
this.title?.setOnLongClickListener { _: View ->
this.title.setOnLongClickListener { _: View ->
startActivity(MainActivity.getStartIntent(activity))
true
}
this.rootView?.findViewById<View>(R.id.button_prev)?.setOnClickListener { _: View -> playbackService?.prev() }
this.rootView.findViewById<View>(R.id.button_prev)?.setOnClickListener { _: View -> playback.service.prev() }
this.playPause = this.rootView?.findViewById<TextView>(R.id.button_play_pause)
this.playPause = this.rootView.findViewById(R.id.button_play_pause)
this.playPause?.setOnClickListener { _: View ->
if (playbackService?.playbackState == PlaybackState.Stopped) {
playbackService?.playAll()
this.playPause.setOnClickListener { _: View ->
if (playback.service.state == PlaybackState.Stopped) {
playback.service.playAll()
}
else {
playbackService?.pauseOrResume()
playback.service.pauseOrResume()
}
}
this.rootView?.findViewById<View>(R.id.button_next)?.setOnClickListener { _: View -> playbackService?.next() }
this.rootView.findViewById<View>(R.id.button_next)?.setOnClickListener { _: View -> playback.service.next() }
}
private fun rebindUi() {
val state = playbackService?.playbackState
val state = playback.service.state
val playing = state == PlaybackState.Playing
val buffering = state == PlaybackState.Buffering
this.playPause?.setText(if (playing) R.string.button_pause else R.string.button_play)
this.buffering?.visibility = if (buffering) View.VISIBLE else View.GONE
this.playPause.setText(if (playing) R.string.button_pause else R.string.button_play)
this.buffering.visibility = if (buffering) View.VISIBLE else View.GONE
if (state == PlaybackState.Stopped) {
title?.setTextColor(getColorCompat(R.color.theme_disabled_foreground))
title?.setText(R.string.transport_not_playing)
title.setTextColor(getColorCompat(R.color.theme_disabled_foreground))
title.setText(R.string.transport_not_playing)
}
else {
val defaultValue = getString(if (buffering) R.string.buffering else R.string.unknown_title)
title?.text = fallback(playbackService?.playingTrack?.title, defaultValue)
title?.setTextColor(getColorCompat(R.color.theme_green))
title.text = fallback(playback.service.playingTrack.title, defaultValue)
title.setTextColor(getColorCompat(R.color.theme_green))
}
}
@ -125,9 +112,6 @@ class TransportFragment : Fragment() {
companion object {
val TAG = "TransportFragment"
fun newInstance(): TransportFragment {
return TransportFragment()
}
fun newInstance(): TransportFragment = TransportFragment()
}
}

View File

@ -0,0 +1,40 @@
package io.casey.musikcube.remote.ui.shared.mixin
import android.os.Bundle
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.framework.MixinBase
import io.casey.musikcube.remote.injection.DaggerViewComponent
import io.casey.musikcube.remote.injection.DataModule
import io.casey.musikcube.remote.service.websocket.WebSocketService
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
import javax.inject.Inject
class DataProviderMixin : MixinBase() {
@Inject lateinit var wss: WebSocketService
@Inject lateinit var provider: IDataProvider
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)
DaggerViewComponent.builder()
.appComponent(Application.appComponent)
.dataModule(DataModule())
.build()
.inject(this)
}
override fun onResume() {
super.onResume()
provider.attach()
}
override fun onPause() {
super.onPause()
provider.detach()
}
override fun onDestroy() {
super.onDestroy()
provider.destroy()
}
}

View File

@ -0,0 +1,219 @@
package io.casey.musikcube.remote.ui.shared.mixin
import android.app.Activity
import android.content.Intent
import android.view.View
import android.widget.PopupMenu
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.framework.MixinBase
import io.casey.musikcube.remote.injection.DaggerViewComponent
import io.casey.musikcube.remote.injection.DataModule
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.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
import io.casey.musikcube.remote.ui.category.activity.CategoryBrowseActivity
import io.casey.musikcube.remote.ui.shared.extension.showErrorSnackbar
import io.casey.musikcube.remote.ui.shared.extension.showSnackbar
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject
class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
@Inject lateinit var provider: IDataProvider
private var pendingCode = -1
private var completion: ((Long) -> Unit)? = null
init {
DaggerViewComponent.builder()
.appComponent(Application.appComponent)
.dataModule(DataModule())
.build()
.inject(this)
}
override fun onResume() {
super.onResume()
provider.attach()
}
override fun onPause() {
super.onPause()
provider.detach()
}
override fun onDestroy() {
super.onDestroy()
provider.destroy()
}
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
if (pendingCode == request) {
if (result == Activity.RESULT_OK && data != null) {
val playlistId = data.getLongExtra(CategoryBrowseActivity.EXTRA_ID, -1L)
if (playlistId != -1L) {
completion?.invoke(playlistId)
}
}
pendingCode = -1
completion = null
}
super.onActivityResult(request, result, data)
}
fun add(track: ITrack) {
add(listOf(track))
}
fun add(tracks: List<ITrack>) {
showPlaylistChooser { id ->
addWithErrorHandler(provider.appendToPlaylist(id, tracks))
}
}
fun add(categoryType: String, categoryId: Long) {
showPlaylistChooser { id ->
addWithErrorHandler(provider.appendToPlaylist(id, categoryType, categoryId))
}
}
fun add(category: ICategoryValue) {
showPlaylistChooser { id ->
addWithErrorHandler(provider.appendToPlaylist(id, category))
}
}
private fun addWithErrorHandler(observable: Observable<Boolean>) {
val error = R.string.playlist_edit_add_error
observable.subscribeBy(
onNext = { success -> if (success) showSuccess() else showError(error) },
onError = { showError(error) })
}
private fun showPlaylistChooser(callback: (Long) -> Unit) {
completion = callback
pendingCode = REQUEST_ADD_TO_PLAYLIST
val intent = CategoryBrowseActivity.getStartIntent(
activity,
Messages.Category.PLAYLISTS,
CategoryBrowseActivity.NavigationType.Select)
activity.startActivityForResult(intent, pendingCode)
}
fun showForTrack(track: ITrack, anchorView: View)
{
val popup = PopupMenu(activity, anchorView)
popup.inflate(R.menu.item_context_menu)
popup.menu.removeItem(R.id.menu_show_tracks)
popup.setOnMenuItemClickListener { item ->
val intent: Intent? = when (item.itemId) {
R.id.menu_add_to_playlist -> {
add(track)
null
}
R.id.menu_show_albums -> {
AlbumBrowseActivity.getStartIntent(
activity, Messages.Category.ARTIST, track.artistId)
}
R.id.menu_show_artists -> {
TrackListActivity.getStartIntent(
activity,
Messages.Category.ARTIST,
track.artistId)
}
R.id.menu_show_genres -> {
CategoryBrowseActivity.getStartIntent(
activity,
Messages.Category.GENRE,
Messages.Category.ARTIST,
track.artistId)
}
else -> {
null
}
}
if (intent != null) {
activity.startActivity(intent)
}
true
}
popup.show()
}
fun showForCategory(value: ICategoryValue, anchorView: View)
{
val popup = PopupMenu(activity, anchorView)
popup.inflate(R.menu.item_context_menu)
if (value.type != Messages.Category.GENRE) {
popup.menu.removeItem(R.id.menu_show_artists)
}
when (value.type) {
Messages.Category.ARTIST -> popup.menu.removeItem(R.id.menu_show_artists)
Messages.Category.ALBUM -> popup.menu.removeItem(R.id.menu_show_albums)
Messages.Category.GENRE -> popup.menu.removeItem(R.id.menu_show_genres)
}
popup.setOnMenuItemClickListener { item ->
val intent: Intent? = when (item.itemId) {
R.id.menu_add_to_playlist -> {
add(value)
null
}
R.id.menu_show_albums -> {
AlbumBrowseActivity.getStartIntent(activity, value.type, value.id)
}
R.id.menu_show_tracks -> {
TrackListActivity.getStartIntent(activity, value.type, value.id)
}
R.id.menu_show_genres -> {
CategoryBrowseActivity.getStartIntent(
activity, Messages.Category.GENRE, value.type, value.id)
}
R.id.menu_show_artists -> {
CategoryBrowseActivity.getStartIntent(
activity, Messages.Category.ARTIST, value.type, value.id)
}
else -> {
null
}
}
if (intent != null) {
activity.startActivity(intent)
}
true
}
popup.show()
}
private fun showSuccess() {
showSnackbar(
activity.findViewById(android.R.id.content),
R.string.playlist_edit_add_success)
}
private fun showError(message: Int) {
showErrorSnackbar(activity.findViewById(android.R.id.content), message)
}
companion object {
private val REQUEST_ADD_TO_PLAYLIST = 128
}
}

View File

@ -0,0 +1,72 @@
package io.casey.musikcube.remote.ui.shared.mixin
import android.content.Context
import android.content.SharedPreferences
import android.os.Bundle
import android.view.KeyEvent
import io.casey.musikcube.remote.framework.MixinBase
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.playback.PlaybackServiceFactory
import io.casey.musikcube.remote.ui.settings.constants.Prefs
class PlaybackMixin(var listener: (() -> Unit)? = null): MixinBase() {
private lateinit var prefs: SharedPreferences
var service: IPlaybackService = PlaybackServiceFactory.instance(context)
private set
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)
prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
connect()
}
override fun onPause() {
super.onPause()
disconnect()
}
override fun onResume() {
super.onResume()
reload()
}
fun reload() {
if (active) {
disconnect()
connect()
}
}
fun onKeyDown(keyCode: Int): Boolean {
val streaming = prefs.getBoolean(
Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK)
if (streaming) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
service.volumeDown()
return true
}
else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
service.volumeUp()
return true
}
}
return false
}
private fun connect() {
service = PlaybackServiceFactory.instance(context)
val listener = this.listener
if (listener != null) {
service.connect(listener)
}
}
private fun disconnect() {
val listener = this.listener
if (listener != null) {
service.disconnect(listener)
}
}
}

View File

@ -0,0 +1,38 @@
package io.casey.musikcube.remote.ui.shared.mixin
import android.os.Bundle
import com.uacf.taskrunner.Runner
import io.casey.musikcube.remote.framework.MixinBase
class RunnerMixin(private val callbacks: Runner.TaskCallbacks,
private val callingType: Class<Any>): MixinBase()
{
lateinit var runner: Runner
private set
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)
this.runner = Runner.attach(this.context, callingType, callbacks, bundle, null)
}
override fun onResume() {
super.onResume()
runner.resume()
}
override fun onPause() {
super.onPause()
runner.pause()
}
override fun onSaveInstanceState(bundle: Bundle) {
super.onSaveInstanceState(bundle)
runner.saveState(bundle)
}
override fun onDestroy() {
super.onDestroy()
runner.detach(callbacks)
}
}

View File

@ -0,0 +1,48 @@
package io.casey.musikcube.remote.ui.shared.mixin
import android.os.Bundle
import io.casey.musikcube.remote.framework.MixinBase
import io.casey.musikcube.remote.framework.ViewModel
class ViewModelMixin(private val provider: ViewModel.Provider): MixinBase() {
private var viewModel: ViewModel<*>? = null
fun <T: ViewModel<*>> get(): T? = this.viewModel as T?
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)
viewModel = ViewModel.restore(bundle.getLong(EXTRA_VIEW_MODEL_ID, -1))
if (viewModel == null) {
viewModel = provider.createViewModel()
}
}
override fun onResume() {
super.onResume()
viewModel?.onResume()
}
override fun onPause() {
super.onPause()
viewModel?.onPause()
}
override fun onSaveInstanceState(bundle: Bundle) {
super.onSaveInstanceState(bundle)
if (viewModel != null) {
bundle.putLong(EXTRA_VIEW_MODEL_ID, viewModel!!.id)
}
}
override fun onDestroy() {
super.onDestroy()
viewModel?.onDestroy()
}
companion object {
val EXTRA_VIEW_MODEL_ID = "extra_view_model_id"
}
}

View File

@ -3,41 +3,69 @@ package io.casey.musikcube.remote.ui.tracks.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.Menu
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
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.IDataProvider
import io.casey.musikcube.remote.service.websocket.model.ITrack
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.constants.Navigation
import io.casey.musikcube.remote.ui.shared.extension.*
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.model.TrackListSlidingWindow
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow.QueryFactory
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
import io.casey.musikcube.remote.ui.shared.view.EmptyListView.Capability
import io.casey.musikcube.remote.ui.tracks.adapter.TrackListAdapter
import io.casey.musikcube.remote.util.Debouncer
import io.casey.musikcube.remote.ui.shared.constants.Navigation
import io.casey.musikcube.remote.util.Strings
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.activity.Filterable
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
class TrackListActivity : BaseActivity(), Filterable {
private lateinit var tracks: TrackListSlidingWindow
private lateinit var emptyView: EmptyListView
private lateinit var transport: TransportFragment
private lateinit var adapter: TrackListAdapter
private lateinit var data: DataProviderMixin
private lateinit var playback: PlaybackMixin
private var categoryType: String = ""
private var categoryId: Long = 0
private var lastFilter = ""
private var adapter = Adapter()
private val onItemClickListener = { view: View ->
val index = view.tag as Int
if (isValidCategory(categoryType, categoryId)) {
playback.service.play(categoryType, categoryId, index, lastFilter)
}
else {
playback.service.playAll(index, lastFilter)
}
setResult(Navigation.ResponseCode.PLAYBACK_STARTED)
finish()
}
private val onActionClickListener = { view: View ->
val track = view.tag as ITrack
mixin(ItemContextMenuMixin::class.java)?.showForTrack(track, view)
Unit
}
override fun onCreate(savedInstanceState: Bundle?) {
component.inject(this)
data = mixin(DataProviderMixin())
playback = mixin(PlaybackMixin())
mixin(ItemContextMenuMixin(this))
super.onCreate(savedInstanceState)
@ -52,8 +80,11 @@ class TrackListActivity : BaseActivity(), Filterable {
enableUpNavigation()
val queryFactory = createCategoryQueryFactory(categoryType, categoryId)
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
tracks = TrackListSlidingWindow(recyclerView, data.provider, queryFactory)
adapter = TrackListAdapter(tracks, onItemClickListener, onActionClickListener, playback)
setupDefaultRecyclerView(recyclerView, adapter)
emptyView = findViewById(R.id.empty_list_view)
@ -63,8 +94,6 @@ class TrackListActivity : BaseActivity(), Filterable {
it.alternateView = recyclerView
}
tracks = TrackListSlidingWindow(recyclerView, dataProvider, queryFactory)
tracks.setOnMetadataLoadedListener(slidingWindowListener)
transport = addTransportFragment(object: TransportFragment.OnModelChangedListener {
@ -99,8 +128,8 @@ class TrackListActivity : BaseActivity(), Filterable {
}
private fun initObservers() {
disposables.add(dataProvider.observeState().subscribe(
{ states ->
disposables.add(data.provider.observeState().subscribeBy(
onNext = { states ->
val shouldRequery =
states.first === IDataProvider.State.Connected ||
(states.first === IDataProvider.State.Disconnected && isOfflineTracks)
@ -113,7 +142,8 @@ class TrackListActivity : BaseActivity(), Filterable {
emptyView.update(states.first, adapter.itemCount)
}
},
{ /* error */ }))
onError = {
}))
}
private val filterDebouncer = object : Debouncer<String>(350) {
@ -124,70 +154,6 @@ class TrackListActivity : BaseActivity(), Filterable {
}
}
private val onItemClickListener = { view: View ->
val index = view.tag as Int
if (isValidCategory(categoryType, categoryId)) {
playbackService?.play(categoryType, categoryId, index, lastFilter)
}
else {
playbackService?.playAll(index, lastFilter)
}
setResult(Navigation.ResponseCode.PLAYBACK_STARTED)
finish()
}
private inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.title)
private val subtitle: TextView = itemView.findViewById(R.id.subtitle)
internal fun bind(track: ITrack?, position: Int) {
itemView.tag = position
var titleColor = R.color.theme_foreground
var subtitleColor = R.color.theme_disabled_foreground
if (track != null) {
val playing = transport.playbackService!!.playingTrack
val entryExternalId = track.externalId
val playingExternalId = playing.externalId
if (entryExternalId == playingExternalId) {
titleColor = R.color.theme_green
subtitleColor = R.color.theme_yellow
}
title.text = fallback(track.title, "-")
subtitle.text = fallback(track.albumArtist, "-")
}
else {
title.text = "-"
subtitle.text = "-"
}
title.setTextColor(getColorCompat(titleColor))
subtitle.setTextColor(getColorCompat(subtitleColor))
}
}
private inner class Adapter : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
view.setOnClickListener(onItemClickListener)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(tracks.getTrack(position), position)
}
override fun getItemCount(): Int {
return tracks.count
}
}
private val emptyMessage: String
get() {
if (isOfflineTracks) {
@ -210,48 +176,40 @@ class TrackListActivity : BaseActivity(), Filterable {
if (isValidCategory(categoryType, categoryId)) {
/* tracks for a specified category (album, artists, genres, etc */
return object : QueryFactory() {
override fun count(): Observable<Int> {
return dataProvider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter)
}
override fun count(): Observable<Int> =
data.provider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter)
override fun all(): Observable<List<ITrack>>? {
return dataProvider.getTracksByCategory(categoryType ?: "", categoryId, lastFilter)
}
override fun all(): Observable<List<ITrack>>? =
data.provider.getTracksByCategory(categoryType ?: "", categoryId, lastFilter)
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> {
return dataProvider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter)
}
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> =
data.provider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter)
override fun offline(): Boolean {
return Messages.Category.OFFLINE == categoryType
}
override fun offline(): Boolean =
Messages.Category.OFFLINE == categoryType
}
}
else {
/* all tracks */
return object : QueryFactory() {
override fun count(): Observable<Int> {
return dataProvider.getTrackCount(lastFilter)
}
override fun count(): Observable<Int> =
data.provider.getTrackCount(lastFilter)
override fun all(): Observable<List<ITrack>>? {
return dataProvider.getTracks(lastFilter)
}
override fun all(): Observable<List<ITrack>>? =
data.provider.getTracks(lastFilter)
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> {
return dataProvider.getTracks(limit, offset, lastFilter)
}
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> =
data.provider.getTracks(limit, offset, lastFilter)
override fun offline(): Boolean {
return Messages.Category.OFFLINE == categoryType
}
override fun offline(): Boolean =
Messages.Category.OFFLINE == categoryType
}
}
}
private val slidingWindowListener = object : TrackListSlidingWindow.OnMetadataLoadedListener {
override fun onReloaded(count: Int) {
emptyView.update(dataProvider.state, count)
emptyView.update(data.provider.state, count)
}
override fun onMetadataLoaded(offset: Int, count: Int) {}
@ -285,12 +243,10 @@ class TrackListActivity : BaseActivity(), Filterable {
return intent
}
fun getStartIntent(context: Context): Intent {
return Intent(context, TrackListActivity::class.java)
}
fun getStartIntent(context: Context): Intent =
Intent(context, TrackListActivity::class.java)
private fun isValidCategory(categoryType: String?, categoryId: Long): Boolean {
return categoryType != null && categoryType.isNotEmpty() && categoryId != -1L
}
private fun isValidCategory(categoryType: String?, categoryId: Long): Boolean =
categoryType != null && categoryType.isNotEmpty() && categoryId != -1L
}
}

View File

@ -0,0 +1,71 @@
package io.casey.musikcube.remote.ui.tracks.adapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.playback.IPlaybackService
import io.casey.musikcube.remote.service.websocket.model.ITrack
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
class TrackListAdapter(private val tracks: TrackListSlidingWindow,
private val onItemClickListener: (View) -> Unit,
private val onActionClickListener: (View) -> Unit,
private var playback: PlaybackMixin) : RecyclerView.Adapter<TrackListAdapter.ViewHolder>()
{
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackListAdapter.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
view.setOnClickListener(onItemClickListener)
view.findViewById<View>(R.id.action).setOnClickListener(onActionClickListener)
return ViewHolder(view, playback)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(tracks.getTrack(position), position)
}
override fun getItemCount(): Int = tracks.count
class ViewHolder internal constructor(private val view: View,
private val playback: PlaybackMixin) : RecyclerView.ViewHolder(view)
{
private val title: TextView = view.findViewById(R.id.title)
private val subtitle: TextView = view.findViewById(R.id.subtitle)
private val action: View = view.findViewById(R.id.action)
internal fun bind(track: ITrack?, position: Int) {
itemView.tag = position
action.tag = track
var titleColor = R.color.theme_foreground
var subtitleColor = R.color.theme_disabled_foreground
if (track != null) {
val playing = playback.service.playingTrack
val entryExternalId = track.externalId
val playingExternalId = playing.externalId
if (entryExternalId == playingExternalId) {
titleColor = R.color.theme_green
subtitleColor = R.color.theme_yellow
}
title.text = fallback(track.title, "-")
subtitle.text = fallback(track.albumArtist, "-")
}
else {
title.text = "-"
subtitle.text = "-"
}
title.setTextColor(getColorCompat(titleColor))
subtitle.setTextColor(getColorCompat(subtitleColor))
}
}
}

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
android:tint="?attr/colorControlNormal">
<path
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2c-1.1,0 -2,0.9 -2,2S10.9,8 12,8zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2c1.1,0 2,-0.9 2,-2S13.1,10 12,10zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2c1.1,0 2,-0.9 2,-2S13.1,16 12,16z"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:drawable="@drawable/ic_overflow"/>
</ripple>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/theme_button_background"/>
</selector>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/theme_background"/>
</selector>

View File

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/theme_selected_background"/>
</selector>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@color/theme_button_background"/>
</selector>

View File

@ -1,17 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:background="?android:selectableItemBackground"
android:minHeight="52dp"
android:padding="8dp">
android:orientation="horizontal"
android:minHeight="52dp">
<ImageView
android:id="@+id/artwork"
android:layout_width="52dp"
android:layout_height="52dp"
android:visibility="gone" />
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_weight="1.0"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
android:layout_gravity="center_vertical"
android:padding="8dp">
<TextView
android:id="@+id/title"
@ -22,7 +30,7 @@
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/theme_foreground"
android:text="title"/>
tools:text="title"/>
<TextView
android:textSize="12dp"
@ -34,8 +42,19 @@
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/theme_disabled_foreground"
android:text="subtitle"/>
tools:text="subtitle"/>
</LinearLayout>
</FrameLayout>
<ImageView
android:id="@+id/action"
android:background="?android:selectableItemBackground"
android:src="@drawable/ic_overflow"
android:padding="8dp"
android:layout_gravity="center_vertical"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_margin="8dp"
android:gravity="center"/>
</LinearLayout>

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_add_to_playlist"
android:title="@string/menu_add_to_playlist"/>
<item
android:id="@+id/menu_show_artists"
android:title="@string/menu_show_artists"/>
<item
android:id="@+id/menu_show_albums"
android:title="@string/menu_show_albums"/>
<item
android:id="@+id/menu_show_genres"
android:title="@string/menu_show_genres"/>
<item
android:id="@+id/menu_show_tracks"
android:title="@string/menu_show_tracks"/>
</menu>

View File

@ -63,6 +63,11 @@
<string name="menu_playlists">playlists</string>
<string name="menu_remote_toggle">remote playback</string>
<string name="menu_offline_tracks">offline songs</string>
<string name="menu_add_to_playlist">add to playlist</string>
<string name="menu_show_tracks">songs</string>
<string name="menu_show_albums">albums</string>
<string name="menu_show_artists">artist</string>
<string name="menu_show_genres">genres</string>
<string name="unknown_value">&lt;unknown&gt;</string>
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
<string name="snackbar_remote_enabled">switched to remote control mode</string>
@ -110,4 +115,8 @@
<string name="update_check_dialog_message">\nmusikbox version %s is now available. would you like to download it now?</string>
<string name="update_check_dont_ask_again">don\'t ask me again for this version</string>
<string name="buffering">buffering</string>
<string name="playlist_edit_no_playlists">couldn\'t get playlists from server</string>
<string name="playlist_edit_add_error">playlist update failed</string>
<string name="playlist_edit_add_success">playlist updated</string>
<string name="playlist_edit_list_title">pick a playlist</string>
</resources>