diff --git a/src/musikdroid/app/build.gradle b/src/musikdroid/app/build.gradle index b0c26de00..4e0e3fc2a 100644 --- a/src/musikdroid/app/build.gradle +++ b/src/musikdroid/app/build.gradle @@ -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' diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/IComponent.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/IMixin.kt similarity index 55% rename from src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/IComponent.kt rename to src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/IMixin.kt index 3364343bc..a5137050d 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/IComponent.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/IMixin.kt @@ -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() } \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/ComponentBase.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/MixinBase.kt similarity index 63% rename from src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/ComponentBase.kt rename to src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/MixinBase.kt index 19020aa47..8e9078128 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/ComponentBase.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/MixinBase.kt @@ -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) { } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/MixinSet.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/MixinSet.kt new file mode 100644 index 000000000..ad14fcce9 --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/MixinSet.kt @@ -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, IMixin> = mutableMapOf() + private var bundle = Bundle() + + fun 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 get(cls: Class): 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() } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/ViewModel.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/ViewModel.kt new file mode 100644 index 000000000..50660ac98 --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/ViewModel.kt @@ -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(protected val runner: Runner? = null): Runner.TaskCallbacks { + val id: Long = nextId.incrementAndGet() + + interface Provider { + fun > 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>() + + fun > restore(id: Long): T? { + val instance: T? = idToInstance[id] as T? + if (instance != null) { + handler.removeCallbacks(instance.cleanup) + } + return instance + } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/ComponentSet.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/ComponentSet.kt deleted file mode 100644 index 1e3c138b3..000000000 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/framework/components/ComponentSet.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.casey.musikcube.remote.framework.components - -import android.os.Bundle - -class ComponentSet : ComponentBase() { - private val components: MutableMap, 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 get(cls: Class): 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() } - } -} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/injection/ViewComponent.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/injection/ViewComponent.kt index 18066611b..47c088e40 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/injection/ViewComponent.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/injection/ViewComponent.kt @@ -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) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/IPlaybackService.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/IPlaybackService.kt index 7130afa5c..a283c0630 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/IPlaybackService.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/IPlaybackService.kt @@ -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 diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt index 677275ea4..102ce8565 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt @@ -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 { - return dataProvider.getPlayQueueTracksCount() - } - - override fun all(): Observable>? { - return dataProvider.getPlayQueueTracks() - } - - override fun page(offset: Int, limit: Int): Observable> { - return dataProvider.getPlayQueueTracks(limit, offset) - } - - override fun offline(): Boolean { - return false - } + override fun count(): Observable = dataProvider.getPlayQueueTracksCount() + override fun all(): Observable>? = dataProvider.getPlayQueueTracks() + override fun page(offset: Int, limit: Int): Observable> = dataProvider.getPlayQueueTracks(limit, offset) + override fun offline(): Boolean = false } private val client = object : WebSocketService.Client { diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt index 8eaafe0e8..ac6c6ac79 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt @@ -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 -> { } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/system/SystemService.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/system/SystemService.kt index 4d94f716f..ebf97b0b4 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/system/SystemService.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/system/SystemService.kt @@ -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 diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt index a768fcc1e..a5f9a39e9 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt @@ -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" diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt index 44234c9f7..ff48502e7 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt @@ -31,13 +31,14 @@ interface IDataProvider { fun getPlaylists(): Observable> - fun getCategoryValues(type: String, filter: String = ""): Observable> + fun getCategoryValues(type: String, predicateType: String = "", predicateId: Long = -1L, filter: String = ""): Observable> fun createPlaylist(playlistName: String, categoryType: String = "", categoryId: Long = -1, filter: String = ""): Observable fun createPlaylist(playlistName: String, tracks: List = ArrayList()): Observable fun createPlaylistWithExternalIds(playlistName: String, externalIds: List = ArrayList()): Observable fun appendToPlaylist(playlistId: Long, categoryType: String = "", categoryId: Long = -1, filter: String = "", offset: Long = -1): Observable fun appendToPlaylist(playlistId: Long, tracks: List = ArrayList(), offset: Long = -1): Observable + fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable fun appendToPlaylistWithExternalIds(playlistId: Long, externalIds: List = ArrayList(), offset: Long = -1): Observable fun renamePlaylist(playlistId: Long, newName: String): Observable fun deletePlaylist(playlistId: Long): Observable diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt index 0ed09da52..0ae769003 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt @@ -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> { + override fun getCategoryValues(type: String, predicateType: String, predicateId: Long, filter: String): Observable> { 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 = + appendToPlaylist(playlistId, categoryValue.type, categoryValue.id) + override fun appendToPlaylistWithExternalIds(playlistId: Long, externalIds: List, offset: Long): Observable { val jsonArray = JSONArray() externalIds.forEach { jsonArray.put(it) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteTrack.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteTrack.kt index ea9b994b9..1342d0d7e 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteTrack.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteTrack.kt @@ -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 = mapOf( diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/albums/activity/AlbumBrowseActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/albums/activity/AlbumBrowseActivity.kt index fc8d142ed..75510aef6 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/albums/activity/AlbumBrowseActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/albums/activity/AlbumBrowseActivity.kt @@ -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(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(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(R.id.title) - private val subtitle = itemView.findViewById(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() { - private var model: List = listOf() - - internal fun setModel(model: List) { - 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) } } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/albums/adapter/AlbumBrowseAdapter.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/albums/adapter/AlbumBrowseAdapter.kt new file mode 100644 index 000000000..c2ef06e7f --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/albums/adapter/AlbumBrowseAdapter.kt @@ -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() +{ + interface EventListener { + fun onItemClicked(album: IAlbum) + fun onActionClicked(view: View, album: IAlbum) + } + + private var model: List = listOf() + + internal fun setModel(model: List) { + 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(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(R.id.title) + private val subtitle = itemView.findViewById(R.id.subtitle) + private val artwork = itemView.findViewById(R.id.artwork) + private val action = itemView.findViewById(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 + } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/category/activity/CategoryBrowseActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/category/activity/CategoryBrowseActivity.kt index 74554d000..1ed9e4fb3 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/category/activity/CategoryBrowseActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/category/activity/CategoryBrowseActivity.kt @@ -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(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(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() { - private var model: List = ArrayList() - - internal fun setModel(model: List?) { - 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 = 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) } } } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/category/adapter/CategoryBrowseAdapter.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/category/adapter/CategoryBrowseAdapter.kt new file mode 100644 index 000000000..74a2e8c39 --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/category/adapter/CategoryBrowseAdapter.kt @@ -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() +{ + interface EventListener { + fun onItemClicked(value: ICategoryValue) + fun onActionClicked(view: View, value: ICategoryValue) + } + + private var model: List = ArrayList() + + internal fun setModel(model: List?) { + 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(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(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 + } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt index d549bcf4f..162d60415 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt @@ -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(R.id.button_prev).setOnClickListener { _: View -> playback?.prev() } + findViewById(R.id.button_prev).setOnClickListener { _: View -> playback.service.prev() } findViewById(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(R.id.button_next).setOnClickListener { _: View -> playback?.next() } + findViewById(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(R.id.button_play_queue).setOnClickListener { _ -> navigateToPlayQueue() } findViewById(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() } } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/view/MainMetadataView.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/view/MainMetadataView.kt index 27c5f7a7a..4b7d4331c 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/view/MainMetadataView.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/view/MainMetadataView.kt @@ -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) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/activity/PlayQueueActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/activity/PlayQueueActivity.kt index 77c2fcaff..de23fca4a 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/activity/PlayQueueActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/activity/PlayQueueActivity.kt @@ -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(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) {} diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/settings/activity/SettingsActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/settings/activity/SettingsActivity.kt index 567eb6826..5e6cd4ce5 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/settings/activity/SettingsActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/settings/activity/SettingsActivity.kt @@ -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 } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/activity/BaseActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/activity/BaseActivity.kt index aec963d43..cab6e7b68 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/activity/BaseActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/activity/BaseActivity.kt @@ -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 component(cls: Class): T? = components.get(cls) - protected val socketService: WebSocketService get() = wss - - protected var playbackService: IPlaybackService? = null - private set + override fun > createViewModel(): T? = null + protected fun > getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get() as T + protected fun mixin(mixin: T): T = mixins.add(mixin) + protected fun mixin(cls: Class): 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 } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt index 04549fc07..b90c21267 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt @@ -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(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(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!! -} \ No newline at end of file +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(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!! diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/BaseFragment.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/BaseFragment.kt new file mode 100644 index 000000000..71f03d6de --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/BaseFragment.kt @@ -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 > createViewModel(): T? = null + protected fun > getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get() as T + protected fun mixin(mixin: T): T = mixins.add(mixin) + protected fun mixin(cls: Class): T? = mixins.get(cls) +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt index 2a423650d..e8628f905 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/fragment/TransportFragment.kt @@ -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(R.id.track_title) - this.buffering = this.rootView?.findViewById(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(R.id.title_bar) + val titleBar = this.rootView.findViewById(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(R.id.button_prev)?.setOnClickListener { _: View -> playbackService?.prev() } + this.rootView.findViewById(R.id.button_prev)?.setOnClickListener { _: View -> playback.service.prev() } - this.playPause = this.rootView?.findViewById(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(R.id.button_next)?.setOnClickListener { _: View -> playbackService?.next() } + this.rootView.findViewById(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() } } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/DataProviderMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/DataProviderMixin.kt new file mode 100644 index 000000000..cea96539d --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/DataProviderMixin.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt new file mode 100644 index 000000000..121a8970f --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt @@ -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) { + 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) { + 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 + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/PlaybackMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/PlaybackMixin.kt new file mode 100644 index 000000000..5baabe113 --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/PlaybackMixin.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/RunnerMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/RunnerMixin.kt new file mode 100644 index 000000000..b557aac8d --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/RunnerMixin.kt @@ -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): 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) + } +} + diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ViewModelMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ViewModelMixin.kt new file mode 100644 index 000000000..af463c662 --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ViewModelMixin.kt @@ -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 > 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" + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt index 39ad7aeb7..b5878aef6 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt @@ -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(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(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() { - 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 { - return dataProvider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter) - } + override fun count(): Observable = + data.provider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter) - override fun all(): Observable>? { - return dataProvider.getTracksByCategory(categoryType ?: "", categoryId, lastFilter) - } + override fun all(): Observable>? = + data.provider.getTracksByCategory(categoryType ?: "", categoryId, lastFilter) - override fun page(offset: Int, limit: Int): Observable> { - return dataProvider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter) - } + override fun page(offset: Int, limit: Int): Observable> = + 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 { - return dataProvider.getTrackCount(lastFilter) - } + override fun count(): Observable = + data.provider.getTrackCount(lastFilter) - override fun all(): Observable>? { - return dataProvider.getTracks(lastFilter) - } + override fun all(): Observable>? = + data.provider.getTracks(lastFilter) - override fun page(offset: Int, limit: Int): Observable> { - return dataProvider.getTracks(limit, offset, lastFilter) - } + override fun page(offset: Int, limit: Int): Observable> = + 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 } } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/TrackListAdapter.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/TrackListAdapter.kt new file mode 100644 index 000000000..9fc90afd5 --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/TrackListAdapter.kt @@ -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() +{ + 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(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)) + } + } +} diff --git a/src/musikdroid/app/src/main/res/drawable-v21/ic_overflow.xml b/src/musikdroid/app/src/main/res/drawable-v21/ic_overflow.xml new file mode 100644 index 000000000..7f194817a --- /dev/null +++ b/src/musikdroid/app/src/main/res/drawable-v21/ic_overflow.xml @@ -0,0 +1,23 @@ + + + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/drawable-v21/overflow_button.xml b/src/musikdroid/app/src/main/res/drawable-v21/overflow_button.xml new file mode 100644 index 000000000..92d046303 --- /dev/null +++ b/src/musikdroid/app/src/main/res/drawable-v21/overflow_button.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/drawable/playback_checkbox_text.xml b/src/musikdroid/app/src/main/res/drawable-v21/playback_checkbox_text.xml similarity index 100% rename from src/musikdroid/app/src/main/res/drawable/playback_checkbox_text.xml rename to src/musikdroid/app/src/main/res/drawable-v21/playback_checkbox_text.xml diff --git a/src/musikdroid/app/src/main/res/drawable/category_button.xml b/src/musikdroid/app/src/main/res/drawable/category_button.xml deleted file mode 100644 index 762eb7b42..000000000 --- a/src/musikdroid/app/src/main/res/drawable/category_button.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/drawable/not_playing_button.xml b/src/musikdroid/app/src/main/res/drawable/not_playing_button.xml deleted file mode 100644 index aaec763a6..000000000 --- a/src/musikdroid/app/src/main/res/drawable/not_playing_button.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/drawable/playback_button.xml b/src/musikdroid/app/src/main/res/drawable/playback_button.xml deleted file mode 100644 index 1367d27e3..000000000 --- a/src/musikdroid/app/src/main/res/drawable/playback_button.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/drawable/playback_checkbox.xml b/src/musikdroid/app/src/main/res/drawable/playback_checkbox.xml deleted file mode 100644 index c6f43b2c9..000000000 --- a/src/musikdroid/app/src/main/res/drawable/playback_checkbox.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/layout/simple_list_item.xml b/src/musikdroid/app/src/main/res/layout/simple_list_item.xml index 3e68b99cd..2d53c2970 100644 --- a/src/musikdroid/app/src/main/res/layout/simple_list_item.xml +++ b/src/musikdroid/app/src/main/res/layout/simple_list_item.xml @@ -1,17 +1,25 @@ - + android:orientation="horizontal" + android:minHeight="52dp"> + + + android:layout_gravity="center_vertical" + android:padding="8dp"> + tools:text="title"/> + tools:text="subtitle"/> - \ No newline at end of file + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/menu/item_context_menu.xml b/src/musikdroid/app/src/main/res/menu/item_context_menu.xml new file mode 100644 index 000000000..68ccd9327 --- /dev/null +++ b/src/musikdroid/app/src/main/res/menu/item_context_menu.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/values/strings.xml b/src/musikdroid/app/src/main/res/values/strings.xml index c0e50d6f0..89edfe15d 100644 --- a/src/musikdroid/app/src/main/res/values/strings.xml +++ b/src/musikdroid/app/src/main/res/values/strings.xml @@ -63,6 +63,11 @@ playlists remote playback offline songs + add to playlist + songs + albums + artist + genres <unknown> switched to streaming mode switched to remote control mode @@ -110,4 +115,8 @@ \nmusikbox version %s is now available. would you like to download it now? don\'t ask me again for this version buffering + couldn\'t get playlists from server + playlist update failed + playlist updated + pick a playlist