mirror of
https://github.com/clangen/musikcube.git
synced 2024-10-02 13:02:35 +00:00
Intermediate commit with a bunch of important changes:
1. Renamed "Components" to "Mixins" to avoid confusion with Dagger 2. Implemented a bunch of mixins -- DataProviderMixin, ItemContextMenuMixin, PlaybackMixin, RunnerMixin, ViewModelMixin to modularize functionality and slim down base classes 3. Started implementing new context menu for browsing predicated categories 4. Some other general cleanup -- more properties instead of methods, function expressions, etc
This commit is contained in:
parent
b950ae0259
commit
6573194dac
@ -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'
|
||||
|
@ -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()
|
||||
}
|
@ -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) {
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
package io.casey.musikcube.remote.framework
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
|
||||
class MixinSet : MixinBase() {
|
||||
private data class ActivityResult (val request: Int, val result: Int, val data: Intent?)
|
||||
|
||||
private var activityResult: ActivityResult? = null
|
||||
private val components: MutableMap<Class<out IMixin>, IMixin> = mutableMapOf()
|
||||
private var bundle = Bundle()
|
||||
|
||||
fun <T> add(mixin: IMixin): T {
|
||||
components.put(mixin.javaClass, mixin)
|
||||
|
||||
when (state) {
|
||||
State.Created ->
|
||||
mixin.onCreate(bundle)
|
||||
State.Started -> {
|
||||
mixin.onCreate(bundle)
|
||||
mixin.onStart()
|
||||
}
|
||||
State.Resumed -> {
|
||||
mixin.onCreate(bundle)
|
||||
mixin.onStart()
|
||||
mixin.onResume()
|
||||
}
|
||||
State.Paused -> {
|
||||
mixin.onCreate(bundle)
|
||||
mixin.onStart()
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
return mixin as T
|
||||
}
|
||||
|
||||
fun <T: IMixin> get(cls: Class<out T>): T? = components.get(cls) as T?
|
||||
|
||||
override fun onCreate(bundle: Bundle) {
|
||||
super.onCreate(bundle)
|
||||
this.bundle = bundle
|
||||
components.values.forEach { it.onCreate(bundle) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
components.values.forEach { it.onStart() }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
components.values.forEach { it.onResume() }
|
||||
|
||||
val ar = activityResult
|
||||
if (ar != null) {
|
||||
components.values.forEach { it.onActivityResult(ar.request, ar.result, ar.data) }
|
||||
activityResult = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
components.values.forEach { it.onPause() }
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
components.values.forEach { it.onStop() }
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(bundle: Bundle) {
|
||||
super.onSaveInstanceState(bundle)
|
||||
components.values.forEach { it.onSaveInstanceState(bundle) }
|
||||
}
|
||||
|
||||
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
|
||||
super.onActivityResult(request, result, data)
|
||||
if (active) {
|
||||
components.values.forEach { it.onActivityResult(request, result, data) }
|
||||
}
|
||||
else {
|
||||
activityResult = ActivityResult(request, result, data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
components.values.forEach { it.onDestroy() }
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package io.casey.musikcube.remote.framework
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.uacf.taskrunner.Runner
|
||||
import com.uacf.taskrunner.Task
|
||||
import io.casey.musikcube.remote.Application
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
abstract class ViewModel<ListenerT>(protected val runner: Runner? = null): Runner.TaskCallbacks {
|
||||
val id: Long = nextId.incrementAndGet()
|
||||
|
||||
interface Provider {
|
||||
fun <T: ViewModel<*>> createViewModel(): T?
|
||||
}
|
||||
|
||||
protected var listener: ListenerT? = null
|
||||
private set
|
||||
|
||||
fun onPause() {
|
||||
}
|
||||
|
||||
fun onResume() {
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
listener = null
|
||||
handler.postDelayed(cleanup, cleanupDelayMs)
|
||||
}
|
||||
|
||||
fun observe(listener: ListenerT) {
|
||||
this.listener = listener
|
||||
}
|
||||
|
||||
val context: Context = Application.instance!!
|
||||
|
||||
internal val cleanup = object: Runnable {
|
||||
override fun run() {
|
||||
listener = null
|
||||
idToInstance.remove(id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTaskError(name: String?, id: Long, task: Task<*, *>?, error: Throwable?) {
|
||||
}
|
||||
|
||||
override fun onTaskCompleted(name: String?, id: Long, task: Task<*, *>?, result: Any?) {
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val cleanupDelayMs = 3000L
|
||||
private val nextId = AtomicLong(System.currentTimeMillis() + 0)
|
||||
private val handler by lazy { Handler(Looper.getMainLooper()) }
|
||||
private val idToInstance = mutableMapOf<Long, ViewModel<*>>()
|
||||
|
||||
fun <T: ViewModel<*>> restore(id: Long): T? {
|
||||
val instance: T? = idToInstance[id] as T?
|
||||
if (instance != null) {
|
||||
handler.removeCallbacks(instance.cleanup)
|
||||
}
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package io.casey.musikcube.remote.framework.components
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
class ComponentSet : ComponentBase() {
|
||||
private val components: MutableMap<Class<out IComponent>, IComponent> = mutableMapOf()
|
||||
private var bundle = Bundle()
|
||||
|
||||
fun add(component: IComponent) {
|
||||
components.put(component.javaClass, component)
|
||||
|
||||
when (state) {
|
||||
State.Created ->
|
||||
component.onCreate(bundle)
|
||||
State.Started -> {
|
||||
component.onCreate(bundle)
|
||||
component.onStart()
|
||||
}
|
||||
State.Resumed -> {
|
||||
component.onCreate(bundle)
|
||||
component.onStart()
|
||||
component.onResume()
|
||||
}
|
||||
State.Paused -> {
|
||||
component.onCreate(bundle)
|
||||
component.onStart()
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> get(cls: Class<out IComponent>): T? = components.get(cls) as T
|
||||
|
||||
override fun onCreate(bundle: Bundle) {
|
||||
super.onCreate(bundle)
|
||||
this.bundle = bundle
|
||||
components.values.forEach { it.onCreate(bundle) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
components.values.forEach { it.onStart() }
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
components.values.forEach { it.onResume() }
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
components.values.forEach { it.onPause() }
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
components.values.forEach { it.onStop() }
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(bundle: Bundle) {
|
||||
super.onSaveInstanceState(bundle)
|
||||
components.values.forEach { it.onSaveInstanceState(bundle) }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
components.values.forEach { it.onDestroy() }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -2,18 +2,18 @@ package io.casey.musikcube.remote.service.playback.impl.remote
|
||||
|
||||
import android.os.Handler
|
||||
import io.casey.musikcube.remote.Application
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.service.websocket.model.ITrack
|
||||
import io.casey.musikcube.remote.model.impl.remote.RemoteTrack
|
||||
import io.casey.musikcube.remote.injection.DaggerServiceComponent
|
||||
import io.casey.musikcube.remote.injection.DataModule
|
||||
import io.casey.musikcube.remote.model.impl.remote.RemoteTrack
|
||||
import io.casey.musikcube.remote.service.playback.IPlaybackService
|
||||
import io.casey.musikcube.remote.service.playback.PlaybackState
|
||||
import io.casey.musikcube.remote.service.playback.RepeatMode
|
||||
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.SocketMessage
|
||||
import io.casey.musikcube.remote.service.websocket.WebSocketService
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.service.websocket.model.ITrack
|
||||
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
|
||||
import io.reactivex.Observable
|
||||
import org.json.JSONObject
|
||||
import java.util.*
|
||||
@ -49,12 +49,7 @@ class RemotePlaybackService : IPlaybackService {
|
||||
|
||||
internal fun get(track: JSONObject?): Double {
|
||||
if (track != null && track.optLong(Metadata.Track.ID, -1L) == trackId && trackId != -1L) {
|
||||
if (pauseTime != 0.0) {
|
||||
return pauseTime
|
||||
}
|
||||
else {
|
||||
return estimatedTime()
|
||||
}
|
||||
return if (pauseTime != 0.0) pauseTime else estimatedTime()
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
@ -106,27 +101,25 @@ class RemotePlaybackService : IPlaybackService {
|
||||
private val listeners = HashSet<() -> Unit>()
|
||||
private val estimatedTime = EstimatedPosition()
|
||||
|
||||
override var playbackState = PlaybackState.Stopped
|
||||
override var state = PlaybackState.Stopped
|
||||
private set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
override val currentTime: Double
|
||||
get() {
|
||||
return estimatedTime.get(track)
|
||||
}
|
||||
get() = estimatedTime.get(track)
|
||||
|
||||
override var repeatMode: RepeatMode = RepeatMode.None
|
||||
private set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
override var isShuffled: Boolean = false
|
||||
override var shuffled: Boolean = false
|
||||
private set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
override var isMuted: Boolean = false
|
||||
override var muted: Boolean = false
|
||||
private set(value) {
|
||||
field = value
|
||||
}
|
||||
@ -193,13 +186,13 @@ class RemotePlaybackService : IPlaybackService {
|
||||
}
|
||||
|
||||
override fun pause() {
|
||||
if (playbackState != PlaybackState.Paused) {
|
||||
if (state != PlaybackState.Paused) {
|
||||
pauseOrResume()
|
||||
}
|
||||
}
|
||||
|
||||
override fun resume() {
|
||||
if (playbackState != PlaybackState.Playing) {
|
||||
if (state != PlaybackState.Playing) {
|
||||
pauseOrResume()
|
||||
}
|
||||
}
|
||||
@ -296,10 +289,10 @@ class RemotePlaybackService : IPlaybackService {
|
||||
get() = RemoteTrack(track)
|
||||
|
||||
private fun reset() {
|
||||
playbackState = PlaybackState.Stopped
|
||||
state = PlaybackState.Stopped
|
||||
repeatMode = RepeatMode.None
|
||||
isMuted = false
|
||||
isShuffled = isMuted
|
||||
muted = false
|
||||
shuffled = muted
|
||||
volume = 0.0
|
||||
queuePosition = 0
|
||||
queueCount = queuePosition
|
||||
@ -328,9 +321,9 @@ class RemotePlaybackService : IPlaybackService {
|
||||
throw IllegalArgumentException("invalid message!")
|
||||
}
|
||||
|
||||
playbackState = PlaybackState.from(message.getStringOption(Key.STATE))
|
||||
state = PlaybackState.from(message.getStringOption(Key.STATE))
|
||||
|
||||
when (playbackState) {
|
||||
when (state) {
|
||||
PlaybackState.Paused -> estimatedTime.pause()
|
||||
PlaybackState.Playing -> {
|
||||
estimatedTime.resume()
|
||||
@ -340,8 +333,8 @@ class RemotePlaybackService : IPlaybackService {
|
||||
}
|
||||
|
||||
repeatMode = RepeatMode.from(message.getStringOption(Key.REPEAT_MODE))
|
||||
isShuffled = message.getBooleanOption(Key.SHUFFLED)
|
||||
isMuted = message.getBooleanOption(Key.MUTED)
|
||||
shuffled = message.getBooleanOption(Key.SHUFFLED)
|
||||
muted = message.getBooleanOption(Key.MUTED)
|
||||
volume = message.getDoubleOption(Key.VOLUME)
|
||||
queueCount = message.getIntOption(Key.PLAY_QUEUE_COUNT)
|
||||
queuePosition = message.getIntOption(Key.PLAY_QUEUE_POSITION)
|
||||
@ -366,7 +359,7 @@ class RemotePlaybackService : IPlaybackService {
|
||||
private fun scheduleTimeSyncMessage() {
|
||||
handler.removeCallbacks(syncTimeRunnable)
|
||||
|
||||
if (playbackState == PlaybackState.Playing) {
|
||||
if (state == PlaybackState.Playing) {
|
||||
handler.postDelayed(syncTimeRunnable, SYNC_TIME_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
@ -381,21 +374,10 @@ class RemotePlaybackService : IPlaybackService {
|
||||
}
|
||||
|
||||
override val playlistQueryFactory: TrackListSlidingWindow.QueryFactory = object : TrackListSlidingWindow.QueryFactory() {
|
||||
override fun count(): Observable<Int> {
|
||||
return dataProvider.getPlayQueueTracksCount()
|
||||
}
|
||||
|
||||
override fun all(): Observable<List<ITrack>>? {
|
||||
return dataProvider.getPlayQueueTracks()
|
||||
}
|
||||
|
||||
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> {
|
||||
return dataProvider.getPlayQueueTracks(limit, offset)
|
||||
}
|
||||
|
||||
override fun offline(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun count(): Observable<Int> = dataProvider.getPlayQueueTracksCount()
|
||||
override fun all(): Observable<List<ITrack>>? = dataProvider.getPlayQueueTracks()
|
||||
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> = dataProvider.getPlayQueueTracks(limit, offset)
|
||||
override fun offline(): Boolean = false
|
||||
}
|
||||
|
||||
private val client = object : WebSocketService.Client {
|
||||
|
@ -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 -> { }
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -31,13 +31,14 @@ interface IDataProvider {
|
||||
|
||||
fun getPlaylists(): Observable<List<IPlaylist>>
|
||||
|
||||
fun getCategoryValues(type: String, filter: String = ""): Observable<List<ICategoryValue>>
|
||||
fun getCategoryValues(type: String, predicateType: String = "", predicateId: Long = -1L, filter: String = ""): Observable<List<ICategoryValue>>
|
||||
|
||||
fun createPlaylist(playlistName: String, categoryType: String = "", categoryId: Long = -1, filter: String = ""): Observable<Long>
|
||||
fun createPlaylist(playlistName: String, tracks: List<ITrack> = ArrayList()): Observable<Long>
|
||||
fun createPlaylistWithExternalIds(playlistName: String, externalIds: List<String> = ArrayList()): Observable<Long>
|
||||
fun appendToPlaylist(playlistId: Long, categoryType: String = "", categoryId: Long = -1, filter: String = "", offset: Long = -1): Observable<Boolean>
|
||||
fun appendToPlaylist(playlistId: Long, tracks: List<ITrack> = ArrayList(), offset: Long = -1): Observable<Boolean>
|
||||
fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable<Boolean>
|
||||
fun appendToPlaylistWithExternalIds(playlistId: Long, externalIds: List<String> = ArrayList(), offset: Long = -1): Observable<Boolean>
|
||||
fun renamePlaylist(playlistId: Long, newName: String): Observable<Boolean>
|
||||
fun deletePlaylist(playlistId: Long): Observable<Boolean>
|
||||
|
@ -1,9 +1,9 @@
|
||||
package io.casey.musikcube.remote.model.impl.remote
|
||||
|
||||
import io.casey.musikcube.remote.service.websocket.model.*
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.SocketMessage
|
||||
import io.casey.musikcube.remote.service.websocket.WebSocketService
|
||||
import io.casey.musikcube.remote.service.websocket.model.*
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
@ -189,10 +189,12 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
override fun getCategoryValues(type: String, filter: String): Observable<List<ICategoryValue>> {
|
||||
override fun getCategoryValues(type: String, predicateType: String, predicateId: Long, filter: String): Observable<List<ICategoryValue>> {
|
||||
val message = SocketMessage.Builder
|
||||
.request(Messages.Request.QueryCategory)
|
||||
.addOption(Messages.Key.CATEGORY, type)
|
||||
.addOption(Messages.Key.PREDICATE_CATEGORY, predicateType)
|
||||
.addOption(Messages.Key.PREDICATE_ID, predicateId)
|
||||
.addOption(Messages.Key.FILTER, filter)
|
||||
.build()
|
||||
|
||||
@ -293,6 +295,9 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
override fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable<Boolean> =
|
||||
appendToPlaylist(playlistId, categoryValue.type, categoryValue.id)
|
||||
|
||||
override fun appendToPlaylistWithExternalIds(playlistId: Long, externalIds: List<String>, offset: Long): Observable<Boolean> {
|
||||
val jsonArray = JSONArray()
|
||||
externalIds.forEach { jsonArray.put(it) }
|
||||
|
@ -43,9 +43,7 @@ class RemoteTrack(val json: JSONObject) : ITrack {
|
||||
return -1L
|
||||
}
|
||||
|
||||
override fun toJson(): JSONObject {
|
||||
return JSONObject(json.toString())
|
||||
}
|
||||
override fun toJson(): JSONObject = JSONObject(json.toString())
|
||||
|
||||
companion object {
|
||||
private val CATEGORY_NAME_TO_ID: Map<String, String> = mapOf(
|
||||
|
@ -3,38 +3,44 @@ package io.casey.musikcube.remote.ui.albums.activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.model.IAlbum
|
||||
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.ui.shared.extension.*
|
||||
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
|
||||
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
|
||||
import io.casey.musikcube.remote.util.Debouncer
|
||||
import io.casey.musikcube.remote.ui.shared.constants.Navigation
|
||||
import io.casey.musikcube.remote.util.Strings
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
|
||||
import io.casey.musikcube.remote.ui.albums.adapter.AlbumBrowseAdapter
|
||||
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.activity.Filterable
|
||||
import io.casey.musikcube.remote.ui.shared.constants.Navigation
|
||||
import io.casey.musikcube.remote.ui.shared.extension.*
|
||||
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
|
||||
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
|
||||
import io.casey.musikcube.remote.util.Debouncer
|
||||
import io.casey.musikcube.remote.util.Strings
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
|
||||
class AlbumBrowseActivity : BaseActivity(), Filterable {
|
||||
private var adapter: Adapter = Adapter()
|
||||
private var categoryName: String = ""
|
||||
private var categoryId: Long = 0
|
||||
private var lastFilter = ""
|
||||
private lateinit var adapter: AlbumBrowseAdapter
|
||||
private lateinit var playback: PlaybackMixin
|
||||
private lateinit var data: DataProviderMixin
|
||||
private lateinit var transport: TransportFragment
|
||||
private lateinit var emptyView: EmptyListView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
component.inject(this)
|
||||
data = mixin(DataProviderMixin())
|
||||
playback = mixin(PlaybackMixin())
|
||||
mixin(ItemContextMenuMixin(this))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@ -46,6 +52,8 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
|
||||
setTitleFromIntent(R.string.albums_title)
|
||||
enableUpNavigation()
|
||||
|
||||
adapter = AlbumBrowseAdapter(eventListener, playback)
|
||||
|
||||
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
|
||||
setupDefaultRecyclerView(recyclerView, adapter)
|
||||
|
||||
@ -86,8 +94,8 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
|
||||
private fun initObservables() {
|
||||
disposables.add(dataProvider.observeState().subscribe(
|
||||
{ state ->
|
||||
disposables.add(data.provider.observeState().subscribeBy(
|
||||
onNext = { state ->
|
||||
if (state.first == IDataProvider.State.Connected) {
|
||||
filterDebouncer.call()
|
||||
requery()
|
||||
@ -95,15 +103,20 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
|
||||
else {
|
||||
emptyView.update(state.first, adapter.itemCount)
|
||||
}
|
||||
}, { /* error */ }))
|
||||
},
|
||||
onError = {
|
||||
}))
|
||||
}
|
||||
|
||||
private fun requery() {
|
||||
dataProvider.getAlbumsForCategory(categoryName, categoryId, lastFilter)
|
||||
.subscribe({ albumList ->
|
||||
adapter.setModel(albumList)
|
||||
emptyView.update(dataProvider.state, adapter.itemCount)
|
||||
}, { /* error*/ })
|
||||
data.provider.getAlbumsForCategory(categoryName, categoryId, lastFilter)
|
||||
.subscribeBy(
|
||||
onNext = { albumList ->
|
||||
adapter.setModel(albumList)
|
||||
emptyView.update(data.provider.state, adapter.itemCount)
|
||||
},
|
||||
onError = {
|
||||
})
|
||||
}
|
||||
|
||||
private val filterDebouncer = object : Debouncer<String>(350) {
|
||||
@ -114,61 +127,15 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
}
|
||||
|
||||
private val onItemClickListener = { view: View ->
|
||||
val album = view.tag as IAlbum
|
||||
|
||||
val intent = TrackListActivity.getStartIntent(
|
||||
private val eventListener = object: AlbumBrowseAdapter.EventListener {
|
||||
override fun onItemClicked(album: IAlbum) {
|
||||
val intent = TrackListActivity.getStartIntent(
|
||||
this@AlbumBrowseActivity, Messages.Category.ALBUM, album.id, album.value)
|
||||
|
||||
startActivityForResult(intent, Navigation.RequestCode.ALBUM_TRACKS_ACTIVITY)
|
||||
}
|
||||
startActivityForResult(intent, Navigation.RequestCode.ALBUM_TRACKS_ACTIVITY) }
|
||||
|
||||
private inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val title = itemView.findViewById<TextView>(R.id.title)
|
||||
private val subtitle = itemView.findViewById<TextView>(R.id.subtitle)
|
||||
|
||||
internal fun bind(album: IAlbum) {
|
||||
val playing = transport.playbackService!!.playingTrack
|
||||
val playingId = playing.albumId
|
||||
|
||||
var titleColor = R.color.theme_foreground
|
||||
var subtitleColor = R.color.theme_disabled_foreground
|
||||
|
||||
if (playingId != -1L && album.id == playingId) {
|
||||
titleColor = R.color.theme_green
|
||||
subtitleColor = R.color.theme_yellow
|
||||
}
|
||||
|
||||
title.text = fallback(album.value, "-")
|
||||
title.setTextColor(getColorCompat(titleColor))
|
||||
|
||||
subtitle.text = fallback(album.albumArtist, "-")
|
||||
subtitle.setTextColor(getColorCompat(subtitleColor))
|
||||
itemView.tag = album
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Adapter : RecyclerView.Adapter<ViewHolder>() {
|
||||
private var model: List<IAlbum> = listOf()
|
||||
|
||||
internal fun setModel(model: List<IAlbum>) {
|
||||
this.model = model
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
|
||||
view.setOnClickListener(onItemClickListener)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(model[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return model.size
|
||||
override fun onActionClicked(view: View, album: IAlbum) {
|
||||
mixin(ItemContextMenuMixin::class.java)?.showForCategory(album, view)
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,9 +143,8 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
|
||||
private val EXTRA_CATEGORY_NAME = "extra_category_name"
|
||||
private val EXTRA_CATEGORY_ID = "extra_category_id"
|
||||
|
||||
fun getStartIntent(context: Context): Intent {
|
||||
return Intent(context, AlbumBrowseActivity::class.java)
|
||||
}
|
||||
fun getStartIntent(context: Context): Intent =
|
||||
Intent(context, AlbumBrowseActivity::class.java)
|
||||
|
||||
fun getStartIntent(context: Context, categoryName: String, categoryId: Long): Intent {
|
||||
return Intent(context, AlbumBrowseActivity::class.java)
|
||||
@ -198,8 +164,7 @@ class AlbumBrowseActivity : BaseActivity(), Filterable {
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getStartIntent(context: Context, categoryName: String, categoryValue: ICategoryValue): Intent {
|
||||
return getStartIntent(context, categoryName, categoryValue.id, categoryValue.value)
|
||||
}
|
||||
fun getStartIntent(context: Context, categoryName: String, categoryValue: ICategoryValue): Intent =
|
||||
getStartIntent(context, categoryName, categoryValue.id, categoryValue.value)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,81 @@
|
||||
package io.casey.musikcube.remote.ui.albums.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.injection.GlideApp
|
||||
import io.casey.musikcube.remote.service.websocket.model.IAlbum
|
||||
import io.casey.musikcube.remote.ui.shared.extension.fallback
|
||||
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.model.albumart.Size
|
||||
import io.casey.musikcube.remote.ui.shared.model.albumart.getUrl
|
||||
|
||||
class AlbumBrowseAdapter(private val listener: EventListener,
|
||||
private val playback: PlaybackMixin)
|
||||
: RecyclerView.Adapter<AlbumBrowseAdapter.ViewHolder>()
|
||||
{
|
||||
interface EventListener {
|
||||
fun onItemClicked(album: IAlbum)
|
||||
fun onActionClicked(view: View, album: IAlbum)
|
||||
}
|
||||
|
||||
private var model: List<IAlbum> = listOf()
|
||||
|
||||
internal fun setModel(model: List<IAlbum>) {
|
||||
this.model = model
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
|
||||
val action = view.findViewById<View>(R.id.action)
|
||||
view.setOnClickListener({ v -> listener.onItemClicked(v.tag as IAlbum) })
|
||||
action.setOnClickListener({ v -> listener.onActionClicked(v, v.tag as IAlbum) })
|
||||
return ViewHolder(view, playback)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(model[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = model.size
|
||||
|
||||
inner class ViewHolder internal constructor(
|
||||
itemView: View, playback: PlaybackMixin) : RecyclerView.ViewHolder(itemView) {
|
||||
private val title = itemView.findViewById<TextView>(R.id.title)
|
||||
private val subtitle = itemView.findViewById<TextView>(R.id.subtitle)
|
||||
private val artwork = itemView.findViewById<ImageView>(R.id.artwork)
|
||||
private val action = itemView.findViewById<View>(R.id.action)
|
||||
|
||||
internal fun bind(album: IAlbum) {
|
||||
val playing = playback.service.playingTrack
|
||||
val playingId = playing.albumId
|
||||
|
||||
var titleColor = R.color.theme_foreground
|
||||
var subtitleColor = R.color.theme_disabled_foreground
|
||||
|
||||
if (playingId != -1L && album.id == playingId) {
|
||||
titleColor = R.color.theme_green
|
||||
subtitleColor = R.color.theme_yellow
|
||||
}
|
||||
|
||||
artwork.visibility = View.VISIBLE
|
||||
|
||||
GlideApp.with(itemView.context).load(getUrl(album, Size.Large)).into(artwork)
|
||||
|
||||
title.text = fallback(album.value, "-")
|
||||
title.setTextColor(getColorCompat(titleColor))
|
||||
|
||||
subtitle.text = fallback(album.albumArtist, "-")
|
||||
subtitle.setTextColor(getColorCompat(subtitleColor))
|
||||
itemView.tag = album
|
||||
action.tag = album
|
||||
}
|
||||
}
|
||||
}
|
@ -3,51 +3,65 @@ package io.casey.musikcube.remote.ui.category.activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.ui.shared.extension.*
|
||||
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
|
||||
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
|
||||
import io.casey.musikcube.remote.util.Debouncer
|
||||
import io.casey.musikcube.remote.ui.shared.constants.Navigation
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
|
||||
import io.casey.musikcube.remote.ui.category.adapter.CategoryBrowseAdapter
|
||||
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.activity.Filterable
|
||||
import io.casey.musikcube.remote.ui.shared.constants.Navigation
|
||||
import io.casey.musikcube.remote.ui.shared.extension.addTransportFragment
|
||||
import io.casey.musikcube.remote.ui.shared.extension.enableUpNavigation
|
||||
import io.casey.musikcube.remote.ui.shared.extension.initSearchMenu
|
||||
import io.casey.musikcube.remote.ui.shared.extension.setupDefaultRecyclerView
|
||||
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
|
||||
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
|
||||
import io.casey.musikcube.remote.util.Debouncer
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import io.casey.musikcube.remote.service.websocket.WebSocketService.State as SocketState
|
||||
|
||||
class CategoryBrowseActivity : BaseActivity(), Filterable {
|
||||
interface DeepLink {
|
||||
enum class NavigationType {
|
||||
Tracks, Albums, Select;
|
||||
|
||||
companion object {
|
||||
val TRACKS = 0
|
||||
val ALBUMS = 1
|
||||
fun get(ordinal: Int) = values()[ordinal]
|
||||
}
|
||||
}
|
||||
|
||||
private var adapter: Adapter = Adapter()
|
||||
private var deepLinkType: Int = 0
|
||||
private lateinit var adapter: CategoryBrowseAdapter
|
||||
private var navigationType: NavigationType = NavigationType.Tracks
|
||||
private var lastFilter: String? = null
|
||||
private lateinit var category: String
|
||||
private lateinit var predicateType: String
|
||||
private var predicateId: Long = -1
|
||||
private lateinit var transport: TransportFragment
|
||||
private lateinit var emptyView: EmptyListView
|
||||
private lateinit var data: DataProviderMixin
|
||||
private lateinit var playback: PlaybackMixin
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
component.inject(this)
|
||||
data = mixin(DataProviderMixin())
|
||||
playback = mixin(PlaybackMixin())
|
||||
mixin(ItemContextMenuMixin(this))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
category = intent.getStringExtra(EXTRA_CATEGORY)
|
||||
deepLinkType = intent.getIntExtra(EXTRA_DEEP_LINK_TYPE, DeepLink.ALBUMS)
|
||||
adapter = Adapter()
|
||||
predicateType = intent.getStringExtra(EXTRA_PREDICATE_TYPE) ?: ""
|
||||
predicateId = intent.getLongExtra(EXTRA_PREDICATE_ID, -1)
|
||||
navigationType = NavigationType.get(intent.getIntExtra(EXTRA_NAVIGATION_TYPE, NavigationType.Albums.ordinal))
|
||||
adapter = CategoryBrowseAdapter(eventListener, playback, category)
|
||||
|
||||
setContentView(R.layout.recycler_view_activity)
|
||||
setTitle(categoryTitleStringId)
|
||||
@ -83,12 +97,12 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (resultCode == Navigation.ResponseCode.PLAYBACK_STARTED) {
|
||||
setResult(Navigation.ResponseCode.PLAYBACK_STARTED)
|
||||
finish()
|
||||
}
|
||||
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun setFilter(filter: String) {
|
||||
@ -97,8 +111,8 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
|
||||
private fun initObservers() {
|
||||
disposables.add(dataProvider.observeState().subscribe(
|
||||
{ states ->
|
||||
disposables.add(data.provider.observeState().subscribeBy(
|
||||
onNext = { states ->
|
||||
when (states.first) {
|
||||
IDataProvider.State.Connected -> {
|
||||
filterDebouncer.cancel()
|
||||
@ -109,25 +123,22 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
else -> { }
|
||||
}
|
||||
}, { /* error */ }
|
||||
))
|
||||
},
|
||||
onError = {
|
||||
}))
|
||||
}
|
||||
|
||||
private val categoryTypeStringId: Int
|
||||
get() {
|
||||
return CATEGORY_NAME_TO_EMPTY_TYPE[category] ?: R.string.unknown_value
|
||||
}
|
||||
get() = CATEGORY_NAME_TO_EMPTY_TYPE[category] ?: R.string.unknown_value
|
||||
|
||||
private val categoryTitleStringId: Int
|
||||
get() {
|
||||
return CATEGORY_NAME_TO_TITLE[category] ?: R.string.unknown_value
|
||||
}
|
||||
get() = CATEGORY_NAME_TO_TITLE[category] ?: R.string.unknown_value
|
||||
|
||||
private fun requery() {
|
||||
dataProvider.getCategoryValues(category, lastFilter ?: "").subscribe(
|
||||
{ values -> adapter.setModel(values) },
|
||||
{ /* error */ },
|
||||
{ emptyView.update(dataProvider.state, adapter.itemCount)})
|
||||
data.provider.getCategoryValues(category, predicateType, predicateId, lastFilter ?: "").subscribeBy(
|
||||
onNext = { values -> adapter.setModel(values) },
|
||||
onError = { },
|
||||
onComplete = { emptyView.update(data.provider.state, adapter.itemCount)})
|
||||
}
|
||||
|
||||
private val filterDebouncer = object : Debouncer<String>(350) {
|
||||
@ -138,25 +149,24 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
}
|
||||
|
||||
private val onItemClickListener = { view: View ->
|
||||
val entry = view.tag as ICategoryValue
|
||||
if (deepLinkType == DeepLink.ALBUMS) {
|
||||
navigateToAlbums(entry)
|
||||
private val eventListener = object: CategoryBrowseAdapter.EventListener {
|
||||
override fun onItemClicked(value: ICategoryValue) {
|
||||
when (navigationType) {
|
||||
NavigationType.Albums -> navigateToAlbums(value)
|
||||
NavigationType.Tracks -> navigateToTracks(value)
|
||||
NavigationType.Select -> {
|
||||
val intent = Intent()
|
||||
.putExtra(EXTRA_CATEGORY, value.type)
|
||||
.putExtra(EXTRA_ID, value.id)
|
||||
setResult(RESULT_OK, intent)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
navigateToTracks(entry)
|
||||
}
|
||||
}
|
||||
|
||||
private val onItemLongClickListener = { view: View ->
|
||||
/* if we deep link to albums by default, long press will get to
|
||||
tracks. if we deep link to tracks, just ignore */
|
||||
var result = false
|
||||
if (deepLinkType == DeepLink.ALBUMS) {
|
||||
navigateToTracks(view.tag as ICategoryValue)
|
||||
result = true
|
||||
override fun onActionClicked(view: View, value: ICategoryValue) {
|
||||
mixin(ItemContextMenuMixin::class.java)?.showForCategory(value, view)
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
private fun navigateToAlbums(entry: ICategoryValue) {
|
||||
@ -171,56 +181,12 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
|
||||
startActivityForResult(intent, Navigation.RequestCode.CATEGORY_TRACKS_ACTIVITY)
|
||||
}
|
||||
|
||||
private inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val title: TextView = itemView.findViewById(R.id.title)
|
||||
|
||||
init {
|
||||
itemView.findViewById<View>(R.id.subtitle).visibility = View.GONE
|
||||
}
|
||||
|
||||
internal fun bind(categoryValue: ICategoryValue) {
|
||||
val playing = transport.playbackService?.playingTrack
|
||||
val playingId = playing?.getCategoryId(category) ?: -1
|
||||
|
||||
var titleColor = R.color.theme_foreground
|
||||
if (playingId != -1L && categoryValue.id == playingId) {
|
||||
titleColor = R.color.theme_green
|
||||
}
|
||||
|
||||
title.text = fallback(categoryValue.value, getString(R.string.unknown_value))
|
||||
title.setTextColor(getColorCompat(titleColor))
|
||||
itemView.tag = categoryValue
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Adapter : RecyclerView.Adapter<ViewHolder>() {
|
||||
private var model: List<ICategoryValue> = ArrayList()
|
||||
|
||||
internal fun setModel(model: List<ICategoryValue>?) {
|
||||
this.model = model ?: ArrayList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
|
||||
view.setOnClickListener(onItemClickListener)
|
||||
view.setOnLongClickListener(onItemLongClickListener)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(model[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return model.size
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val EXTRA_CATEGORY = "extra_category"
|
||||
private val EXTRA_DEEP_LINK_TYPE = "extra_deep_link_type"
|
||||
val EXTRA_CATEGORY = "extra_category"
|
||||
val EXTRA_ID = "extra_id"
|
||||
private val EXTRA_PREDICATE_TYPE = "extra_predicate_type"
|
||||
private val EXTRA_PREDICATE_ID = "extra_predicate_id"
|
||||
private val EXTRA_NAVIGATION_TYPE = "extra_navigation_type"
|
||||
|
||||
private val CATEGORY_NAME_TO_TITLE: Map<String, Int> = mapOf(
|
||||
Messages.Category.ALBUM_ARTIST to R.string.artists_title,
|
||||
@ -236,15 +202,17 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
|
||||
Messages.Category.ALBUM to R.string.browse_type_albums,
|
||||
Messages.Category.PLAYLISTS to R.string.browse_type_playlists)
|
||||
|
||||
fun getStartIntent(context: Context, category: String): Intent {
|
||||
fun getStartIntent(context: Context, category: String, predicateType: String = "", predicateId: Long = -1): Intent {
|
||||
return Intent(context, CategoryBrowseActivity::class.java)
|
||||
.putExtra(EXTRA_CATEGORY, category)
|
||||
.putExtra(EXTRA_PREDICATE_TYPE, predicateType)
|
||||
.putExtra(EXTRA_PREDICATE_ID, predicateId)
|
||||
}
|
||||
|
||||
fun getStartIntent(context: Context, category: String, deepLinkType: Int): Intent {
|
||||
fun getStartIntent(context: Context, category: String, navigationType: NavigationType): Intent {
|
||||
return Intent(context, CategoryBrowseActivity::class.java)
|
||||
.putExtra(EXTRA_CATEGORY, category)
|
||||
.putExtra(EXTRA_DEEP_LINK_TYPE, deepLinkType)
|
||||
.putExtra(EXTRA_NAVIGATION_TYPE, navigationType.ordinal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,76 @@
|
||||
package io.casey.musikcube.remote.ui.category.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
|
||||
import io.casey.musikcube.remote.ui.shared.extension.fallback
|
||||
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
|
||||
class CategoryBrowseAdapter(private val listener: EventListener,
|
||||
private val playback: PlaybackMixin,
|
||||
private val category: String)
|
||||
: RecyclerView.Adapter<CategoryBrowseAdapter.ViewHolder>()
|
||||
{
|
||||
interface EventListener {
|
||||
fun onItemClicked(value: ICategoryValue)
|
||||
fun onActionClicked(view: View, value: ICategoryValue)
|
||||
}
|
||||
|
||||
private var model: List<ICategoryValue> = ArrayList()
|
||||
|
||||
internal fun setModel(model: List<ICategoryValue>?) {
|
||||
this.model = model ?: ArrayList()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
|
||||
val action = view.findViewById<View>(R.id.action)
|
||||
view.setOnClickListener({ v -> listener.onItemClicked(v.tag as ICategoryValue) })
|
||||
action.setOnClickListener({ v -> listener.onActionClicked(v, v.tag as ICategoryValue) })
|
||||
return ViewHolder(view, playback, category)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(model[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = model.size
|
||||
|
||||
class ViewHolder internal constructor(
|
||||
itemView: View,
|
||||
private val playback: PlaybackMixin,
|
||||
private val category: String) : RecyclerView.ViewHolder(itemView)
|
||||
{
|
||||
private val title: TextView = itemView.findViewById(R.id.title)
|
||||
private val action: View = itemView.findViewById(R.id.action)
|
||||
|
||||
init {
|
||||
itemView.findViewById<View>(R.id.subtitle).visibility = View.GONE
|
||||
}
|
||||
|
||||
internal fun bind(categoryValue: ICategoryValue) {
|
||||
action.tag = categoryValue
|
||||
action.visibility = if (category == Messages.Category.PLAYLISTS) View.GONE else View.VISIBLE
|
||||
|
||||
val playing = playback.service.playingTrack
|
||||
val playingId = playing.getCategoryId(category)
|
||||
|
||||
var titleColor = R.color.theme_foreground
|
||||
if (playingId > 0 && categoryValue.id == playingId) {
|
||||
titleColor = R.color.theme_green
|
||||
}
|
||||
|
||||
title.text = fallback(categoryValue.value, R.string.unknown_value)
|
||||
title.setTextColor(getColorCompat(titleColor))
|
||||
itemView.tag = categoryValue
|
||||
}
|
||||
}
|
||||
}
|
@ -18,36 +18,38 @@ import android.widget.CompoundButton
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.service.playback.IPlaybackService
|
||||
import io.casey.musikcube.remote.service.playback.PlaybackState
|
||||
import io.casey.musikcube.remote.service.playback.RepeatMode
|
||||
import io.casey.musikcube.remote.ui.category.activity.*
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.WebSocketService
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
|
||||
import io.casey.musikcube.remote.ui.category.activity.CategoryBrowseActivity
|
||||
import io.casey.musikcube.remote.ui.home.fragment.InvalidPasswordDialogFragment
|
||||
import io.casey.musikcube.remote.ui.home.view.MainMetadataView
|
||||
import io.casey.musikcube.remote.ui.playqueue.activity.PlayQueueActivity
|
||||
import io.casey.musikcube.remote.ui.settings.activity.SettingsActivity
|
||||
import io.casey.musikcube.remote.ui.settings.constants.Prefs
|
||||
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
|
||||
import io.casey.musikcube.remote.ui.shared.extension.setCheckWithoutEvent
|
||||
import io.casey.musikcube.remote.ui.shared.extension.showSnackbar
|
||||
import io.casey.musikcube.remote.ui.home.fragment.InvalidPasswordDialogFragment
|
||||
import io.casey.musikcube.remote.ui.shared.util.UpdateCheck
|
||||
import io.casey.musikcube.remote.ui.home.view.MainMetadataView
|
||||
import io.casey.musikcube.remote.ui.shared.util.Duration
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.ui.settings.constants.Prefs
|
||||
import io.casey.musikcube.remote.service.websocket.WebSocketService
|
||||
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
|
||||
import io.casey.musikcube.remote.ui.playqueue.activity.PlayQueueActivity
|
||||
import io.casey.musikcube.remote.ui.settings.activity.SettingsActivity
|
||||
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.util.UpdateCheck
|
||||
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
|
||||
|
||||
class MainActivity : BaseActivity() {
|
||||
private val handler = Handler()
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private var playback: IPlaybackService? = null
|
||||
|
||||
private var updateCheck: UpdateCheck = UpdateCheck()
|
||||
private var seekbarValue = -1
|
||||
private var blink = 0
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private lateinit var data: DataProviderMixin
|
||||
private lateinit var playback: PlaybackMixin
|
||||
|
||||
/* views */
|
||||
private lateinit var mainLayout: View
|
||||
private lateinit var metadataView: MainMetadataView
|
||||
@ -67,17 +69,18 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
component.inject(this)
|
||||
data = mixin(DataProviderMixin())
|
||||
playback = mixin(PlaybackMixin({ rebindUi() }))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
prefs = this.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
|
||||
playback = playbackService
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
bindEventListeners()
|
||||
|
||||
if (!socketService.hasValidConnection()) {
|
||||
if (!data.wss.hasValidConnection()) {
|
||||
startActivity(SettingsActivity.getStartIntent(this))
|
||||
}
|
||||
}
|
||||
@ -91,7 +94,6 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
playback = playbackService
|
||||
metadataView.onResume()
|
||||
bindCheckBoxEventListeners()
|
||||
rebindUi()
|
||||
@ -106,7 +108,7 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
val connected = socketService.state === WebSocketService.State.Connected
|
||||
val connected = data.wss.state === WebSocketService.State.Connected
|
||||
val streaming = isStreamingSelected
|
||||
|
||||
menu.findItem(R.id.action_playlists).isEnabled = connected
|
||||
@ -137,7 +139,7 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
R.id.action_playlists -> {
|
||||
startActivity(CategoryBrowseActivity.getStartIntent(
|
||||
this, Messages.Category.PLAYLISTS, CategoryBrowseActivity.DeepLink.TRACKS))
|
||||
this, Messages.Category.PLAYLISTS, CategoryBrowseActivity.NavigationType.Tracks))
|
||||
return true
|
||||
}
|
||||
|
||||
@ -150,11 +152,8 @@ class MainActivity : BaseActivity() {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override val playbackServiceEventListener: (() -> Unit)?
|
||||
get() = playbackEvents
|
||||
|
||||
private fun initObservers() {
|
||||
disposables.add(dataProvider.observeState().subscribe(
|
||||
disposables.add(data.provider.observeState().subscribe(
|
||||
{ states ->
|
||||
when (states.first) {
|
||||
IDataProvider.State.Connected -> rebindUi()
|
||||
@ -163,7 +162,7 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
}, { /* error */ }))
|
||||
|
||||
disposables.add(dataProvider.observeAuthFailure().subscribe(
|
||||
disposables.add(data.provider.observeAuthFailure().subscribe(
|
||||
{
|
||||
val tag = InvalidPasswordDialogFragment.TAG
|
||||
if (supportFragmentManager.findFragmentByTag(tag) == null) {
|
||||
@ -198,7 +197,7 @@ class MainActivity : BaseActivity() {
|
||||
val streaming = isStreamingSelected
|
||||
|
||||
if (streaming) {
|
||||
playback?.stop()
|
||||
playback.service.stop()
|
||||
}
|
||||
|
||||
prefs.edit().putBoolean(Prefs.Key.STREAMING_PLAYBACK, !streaming)?.apply()
|
||||
@ -210,8 +209,7 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
showSnackbar(mainLayout, messageId)
|
||||
|
||||
reloadPlaybackService()
|
||||
playback = playbackService
|
||||
playback.reload()
|
||||
|
||||
invalidateOptionsMenu()
|
||||
rebindUi()
|
||||
@ -247,20 +245,20 @@ class MainActivity : BaseActivity() {
|
||||
totalTime = findViewById(R.id.total_time)
|
||||
seekbar = findViewById(R.id.seekbar)
|
||||
|
||||
findViewById<View>(R.id.button_prev).setOnClickListener { _: View -> playback?.prev() }
|
||||
findViewById<View>(R.id.button_prev).setOnClickListener { _: View -> playback.service.prev() }
|
||||
|
||||
findViewById<View>(R.id.button_play_pause).setOnClickListener { _: View ->
|
||||
if (playback?.playbackState === PlaybackState.Stopped) {
|
||||
playback?.playAll()
|
||||
if (playback.service.state === PlaybackState.Stopped) {
|
||||
playback.service.playAll()
|
||||
}
|
||||
else {
|
||||
playback?.pauseOrResume()
|
||||
playback.service.pauseOrResume()
|
||||
}
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.button_next).setOnClickListener { _: View -> playback?.next() }
|
||||
findViewById<View>(R.id.button_next).setOnClickListener { _: View -> playback.service.next() }
|
||||
|
||||
disconnectedButton.setOnClickListener { _ -> socketService.reconnect() }
|
||||
disconnectedButton.setOnClickListener { _ -> data.wss.reconnect() }
|
||||
|
||||
seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
@ -275,7 +273,7 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
if (seekbarValue != -1) {
|
||||
playback?.seekTo(seekbarValue.toDouble())
|
||||
playback.service.seekTo(seekbarValue.toDouble())
|
||||
seekbarValue = -1
|
||||
}
|
||||
}
|
||||
@ -297,7 +295,7 @@ class MainActivity : BaseActivity() {
|
||||
findViewById<View>(R.id.button_play_queue).setOnClickListener { _ -> navigateToPlayQueue() }
|
||||
|
||||
findViewById<View>(R.id.metadata_container).setOnClickListener { _ ->
|
||||
if (playback?.queueCount ?: 0 > 0) {
|
||||
if (playback.service.queueCount > 0) {
|
||||
navigateToPlayQueue()
|
||||
}
|
||||
}
|
||||
@ -310,17 +308,13 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun rebindUi() {
|
||||
if (playback == null) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
|
||||
val playbackState = playback?.playbackState
|
||||
val playbackState = playback.service.state
|
||||
val streaming = prefs.getBoolean(Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK)
|
||||
val connected = socketService.state === WebSocketService.State.Connected
|
||||
val connected = data.wss.state === WebSocketService.State.Connected
|
||||
val stopped = playbackState === PlaybackState.Stopped
|
||||
val playing = playbackState === PlaybackState.Playing
|
||||
val buffering = playbackState === PlaybackState.Buffering
|
||||
val showMetadataView = !stopped && (playback?.queueCount ?: 0) > 0
|
||||
val showMetadataView = !stopped && (playback.service.queueCount) > 0
|
||||
|
||||
/* bottom section: transport controls */
|
||||
playPause.setText(if (playing || buffering) R.string.button_pause else R.string.button_play)
|
||||
@ -328,14 +322,14 @@ class MainActivity : BaseActivity() {
|
||||
connectedNotPlayingContainer.visibility = if (connected && stopped) View.VISIBLE else View.GONE
|
||||
disconnectedOverlay.visibility = if (connected || !stopped) View.GONE else View.VISIBLE
|
||||
|
||||
val repeatMode = playback?.repeatMode
|
||||
val repeatMode = playback.service.repeatMode
|
||||
val repeatChecked = repeatMode !== RepeatMode.None
|
||||
|
||||
repeatCb.text = getString(REPEAT_TO_STRING_ID[repeatMode] ?: R.string.unknown_value)
|
||||
repeatCb.setCheckWithoutEvent(repeatChecked, this.repeatListener)
|
||||
shuffleCb.text = getString(if (streaming) R.string.button_random else R.string.button_shuffle)
|
||||
shuffleCb.setCheckWithoutEvent(playback?.isShuffled ?: false, shuffleListener)
|
||||
muteCb.setCheckWithoutEvent(playback?.isMuted ?: false, muteListener)
|
||||
shuffleCb.setCheckWithoutEvent(playback.service.shuffled, shuffleListener)
|
||||
muteCb.setCheckWithoutEvent(playback.service.muted, muteListener)
|
||||
|
||||
/* middle section: connected, disconnected, and metadata views */
|
||||
connectedNotPlayingContainer.visibility = View.GONE
|
||||
@ -362,7 +356,7 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun navigateToPlayQueue() {
|
||||
startActivity(PlayQueueActivity.getStartIntent(this@MainActivity, playback?.queuePosition ?: 0))
|
||||
startActivity(PlayQueueActivity.getStartIntent(this@MainActivity, playback.service.queuePosition ?: 0))
|
||||
}
|
||||
|
||||
private fun scheduleUpdateTime(immediate: Boolean) {
|
||||
@ -372,17 +366,17 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
private val updateTimeRunnable = object: Runnable {
|
||||
override fun run() {
|
||||
val duration = playback?.duration ?: 0.0
|
||||
val current: Double = if (seekbarValue == -1) playback?.currentTime ?: 0.0 else seekbarValue.toDouble()
|
||||
val duration = playback.service.duration
|
||||
val current: Double = if (seekbarValue == -1) playback.service.currentTime else seekbarValue.toDouble()
|
||||
|
||||
currentTime.text = Duration.format(current)
|
||||
totalTime.text = Duration.format(duration)
|
||||
seekbar.max = duration.toInt()
|
||||
seekbar.progress = current.toInt()
|
||||
seekbar.secondaryProgress = playback?.bufferedTime?.toInt() ?: 0
|
||||
seekbar.secondaryProgress = playback.service.bufferedTime.toInt()
|
||||
|
||||
var currentTimeColor = R.color.theme_foreground
|
||||
if (playback?.playbackState === PlaybackState.Paused) {
|
||||
if (playback.service.state === PlaybackState.Paused) {
|
||||
currentTimeColor =
|
||||
if (++blink % 2 == 0) R.color.theme_foreground
|
||||
else R.color.theme_blink_foreground
|
||||
@ -395,19 +389,19 @@ class MainActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private val muteListener = { _: CompoundButton, b: Boolean ->
|
||||
if (b != playback?.isMuted) {
|
||||
playback?.toggleMute()
|
||||
if (b != playback.service.muted) {
|
||||
playback.service.toggleMute()
|
||||
}
|
||||
}
|
||||
|
||||
private val shuffleListener = { _: CompoundButton, b: Boolean ->
|
||||
if (b != playback?.isShuffled) {
|
||||
playback?.toggleShuffle()
|
||||
if (b != playback.service.shuffled) {
|
||||
playback.service.toggleShuffle()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRepeatListener() {
|
||||
val currentMode = playback?.repeatMode
|
||||
val currentMode = playback.service.repeatMode
|
||||
|
||||
var newMode = RepeatMode.None
|
||||
|
||||
@ -422,7 +416,7 @@ class MainActivity : BaseActivity() {
|
||||
repeatCb.text = getString(REPEAT_TO_STRING_ID[newMode] ?: R.string.unknown_value)
|
||||
repeatCb.setCheckWithoutEvent(checked, repeatListener)
|
||||
|
||||
playback?.toggleRepeatMode()
|
||||
playback.service.toggleRepeatMode()
|
||||
}
|
||||
|
||||
private fun runUpdateCheck() {
|
||||
@ -446,8 +440,6 @@ class MainActivity : BaseActivity() {
|
||||
onRepeatListener()
|
||||
}
|
||||
|
||||
private val playbackEvents = { rebindUi() }
|
||||
|
||||
class UpdateAvailableDialog: DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val inflater = LayoutInflater.from(activity)
|
||||
@ -525,10 +517,7 @@ class MainActivity : BaseActivity() {
|
||||
|
||||
companion object {
|
||||
val TAG = "switch_to_offline_tracks_dialog"
|
||||
|
||||
fun newInstance(): SwitchToOfflineTracksDialog {
|
||||
return SwitchToOfflineTracksDialog()
|
||||
}
|
||||
fun newInstance(): SwitchToOfflineTracksDialog = SwitchToOfflineTracksDialog()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -12,26 +12,29 @@ import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.service.websocket.model.ITrack
|
||||
import io.casey.musikcube.remote.service.playback.IPlaybackService
|
||||
import io.casey.musikcube.remote.ui.shared.extension.*
|
||||
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
|
||||
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.extension.*
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
|
||||
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
|
||||
class PlayQueueActivity : BaseActivity() {
|
||||
private var adapter: Adapter = Adapter()
|
||||
private var offlineQueue: Boolean = false
|
||||
private var playback: IPlaybackService? = null
|
||||
private lateinit var data: DataProviderMixin
|
||||
private lateinit var playback: PlaybackMixin
|
||||
private lateinit var tracks: TrackListSlidingWindow
|
||||
private lateinit var emptyView: EmptyListView
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
component.inject(this)
|
||||
data = mixin(DataProviderMixin())
|
||||
playback = mixin(PlaybackMixin(playbackEvents))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
playback = playbackService
|
||||
|
||||
setContentView(R.layout.recycler_view_activity)
|
||||
|
||||
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
|
||||
@ -42,22 +45,24 @@ class PlayQueueActivity : BaseActivity() {
|
||||
emptyView.emptyMessage = getString(R.string.play_queue_empty)
|
||||
emptyView.alternateView = recyclerView
|
||||
|
||||
val queryFactory = playback!!.playlistQueryFactory
|
||||
offlineQueue = playback!!.playlistQueryFactory.offline()
|
||||
val queryFactory = playback.service.playlistQueryFactory
|
||||
offlineQueue = playback.service.playlistQueryFactory.offline()
|
||||
|
||||
tracks = TrackListSlidingWindow(recyclerView, dataProvider, queryFactory)
|
||||
tracks = TrackListSlidingWindow(recyclerView, data.provider, queryFactory)
|
||||
tracks.setInitialPosition(intent.getIntExtra(EXTRA_PLAYING_INDEX, -1))
|
||||
tracks.setOnMetadataLoadedListener(slidingWindowListener)
|
||||
|
||||
dataProvider.observeState().subscribe(
|
||||
{ states ->
|
||||
data.provider.observeState().subscribeBy(
|
||||
onNext = { states ->
|
||||
if (states.first == IDataProvider.State.Connected) {
|
||||
tracks.requery()
|
||||
}
|
||||
else {
|
||||
emptyView.update(states.first, adapter.itemCount)
|
||||
}
|
||||
}, { /* error */ })
|
||||
},
|
||||
onError = {
|
||||
})
|
||||
|
||||
setTitleFromIntent(R.string.play_queue_title)
|
||||
addTransportFragment()
|
||||
@ -79,13 +84,9 @@ class PlayQueueActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override val playbackServiceEventListener: (() -> Unit)?
|
||||
get() = playbackEvents
|
||||
|
||||
private val onItemClickListener = View.OnClickListener { v ->
|
||||
if (v.tag is Int) {
|
||||
val index = v.tag as Int
|
||||
playback?.playAt(index)
|
||||
playback.service.playAt(v.tag as Int)
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,7 +113,7 @@ class PlayQueueActivity : BaseActivity() {
|
||||
subtitle.text = "-"
|
||||
}
|
||||
else {
|
||||
val playing = playback!!.playingTrack
|
||||
val playing = playback.service.playingTrack
|
||||
val entryExternalId = track.externalId
|
||||
val playingExternalId = playing.externalId
|
||||
|
||||
@ -142,14 +143,12 @@ class PlayQueueActivity : BaseActivity() {
|
||||
holder.bind(tracks.getTrack(position), position)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return tracks.count
|
||||
}
|
||||
override fun getItemCount(): Int = tracks.count
|
||||
}
|
||||
|
||||
private val slidingWindowListener = object : TrackListSlidingWindow.OnMetadataLoadedListener {
|
||||
override fun onReloaded(count: Int) {
|
||||
emptyView.update(dataProvider.state, count)
|
||||
emptyView.update(data.provider.state, count)
|
||||
}
|
||||
|
||||
override fun onMetadataLoaded(offset: Int, count: Int) {}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -1,36 +1,34 @@
|
||||
package io.casey.musikcube.remote.ui.shared.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.media.AudioManager
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import com.uacf.taskrunner.LifecycleDelegate
|
||||
import com.uacf.taskrunner.Runner
|
||||
import com.uacf.taskrunner.Task
|
||||
import io.casey.musikcube.remote.Application
|
||||
import io.casey.musikcube.remote.framework.components.ComponentSet
|
||||
import io.casey.musikcube.remote.framework.components.IComponent
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.injection.*
|
||||
import io.casey.musikcube.remote.service.playback.IPlaybackService
|
||||
import io.casey.musikcube.remote.service.playback.PlaybackServiceFactory
|
||||
import io.casey.musikcube.remote.ui.shared.extension.hideKeyboard
|
||||
import io.casey.musikcube.remote.framework.IMixin
|
||||
import io.casey.musikcube.remote.framework.MixinSet
|
||||
import io.casey.musikcube.remote.framework.ViewModel
|
||||
import io.casey.musikcube.remote.injection.DaggerViewComponent
|
||||
import io.casey.musikcube.remote.injection.DataModule
|
||||
import io.casey.musikcube.remote.injection.ViewComponent
|
||||
import io.casey.musikcube.remote.ui.settings.constants.Prefs
|
||||
import io.casey.musikcube.remote.service.websocket.WebSocketService
|
||||
import io.casey.musikcube.remote.ui.shared.extension.hideKeyboard
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.RunnerMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.ViewModelMixin
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity(), Runner.TaskCallbacks {
|
||||
abstract class BaseActivity : AppCompatActivity(), ViewModel.Provider, Runner.TaskCallbacks {
|
||||
protected var disposables = CompositeDisposable()
|
||||
private lateinit var runnerDelegate: LifecycleDelegate
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private var paused = false
|
||||
private val components = ComponentSet()
|
||||
@Inject lateinit var wss: WebSocketService
|
||||
@Inject lateinit var dataProvider: IDataProvider
|
||||
private val mixins = MixinSet()
|
||||
|
||||
protected val component: ViewComponent =
|
||||
DaggerViewComponent.builder()
|
||||
@ -40,91 +38,56 @@ abstract class BaseActivity : AppCompatActivity(), Runner.TaskCallbacks {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
component.inject(this)
|
||||
|
||||
mixin(RunnerMixin(this, javaClass))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
components.onCreate(savedInstanceState ?: Bundle())
|
||||
volumeControlStream = AudioManager.STREAM_MUSIC
|
||||
runnerDelegate = LifecycleDelegate(this, this, javaClass, null)
|
||||
runnerDelegate.onCreate(savedInstanceState)
|
||||
playbackService = PlaybackServiceFactory.instance(this)
|
||||
prefs = getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
|
||||
volumeControlStream = AudioManager.STREAM_MUSIC
|
||||
mixins.onCreate(savedInstanceState ?: Bundle())
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
components.onStart()
|
||||
mixins.onStart()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
components.onResume()
|
||||
dataProvider.attach()
|
||||
runnerDelegate.onResume()
|
||||
|
||||
playbackService = PlaybackServiceFactory.instance(this)
|
||||
|
||||
val playbackListener = playbackServiceEventListener
|
||||
if (playbackListener != null) {
|
||||
this.playbackService?.connect(playbackServiceEventListener!!)
|
||||
}
|
||||
|
||||
mixins.onResume()
|
||||
paused = false
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
hideKeyboard()
|
||||
|
||||
super.onPause()
|
||||
|
||||
components.onPause()
|
||||
dataProvider.detach()
|
||||
runnerDelegate.onPause()
|
||||
|
||||
val playbackListener = playbackServiceEventListener
|
||||
if (playbackListener != null) {
|
||||
playbackService?.disconnect(playbackServiceEventListener!!)
|
||||
}
|
||||
|
||||
mixins.onPause()
|
||||
disposables.dispose()
|
||||
disposables = CompositeDisposable()
|
||||
|
||||
paused = true
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
components.onStop()
|
||||
mixins.onStop()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
mixins.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
components.onSaveInstanceState(outState)
|
||||
runnerDelegate.onSaveInstanceState(outState)
|
||||
mixins.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
components.onDestroy()
|
||||
runnerDelegate.onDestroy()
|
||||
dataProvider.destroy()
|
||||
mixins.onDestroy()
|
||||
}
|
||||
|
||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
||||
val streaming = prefs.getBoolean(
|
||||
Prefs.Key.STREAMING_PLAYBACK, Prefs.Default.STREAMING_PLAYBACK)
|
||||
|
||||
/* if we're not streaming we want the hardware buttons to go out to the system */
|
||||
if (!streaming) {
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
|
||||
playbackService?.volumeDown()
|
||||
return true
|
||||
}
|
||||
else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
playbackService?.volumeUp()
|
||||
return true
|
||||
}
|
||||
if (mixin(PlaybackMixin::class.java)?.onKeyDown(keyCode) == true) {
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onKeyDown(keyCode, event)
|
||||
@ -140,41 +103,18 @@ abstract class BaseActivity : AppCompatActivity(), Runner.TaskCallbacks {
|
||||
}
|
||||
|
||||
override fun onTaskCompleted(taskName: String, taskId: Long, task: Task<*, *>, result: Any) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTaskError(s: String, l: Long, task: Task<*, *>, throwable: Throwable) {
|
||||
|
||||
}
|
||||
|
||||
protected fun isPaused(): Boolean = paused
|
||||
protected fun component(component: IComponent) = components.add(component)
|
||||
protected fun <T> component(cls: Class<out IComponent>): T? = components.get(cls)
|
||||
|
||||
protected val socketService: WebSocketService get() = wss
|
||||
|
||||
protected var playbackService: IPlaybackService? = null
|
||||
private set
|
||||
override fun <T: ViewModel<*>> createViewModel(): T? = null
|
||||
protected fun <T: ViewModel<*>> getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get<T>() as T
|
||||
protected fun <T: IMixin> mixin(mixin: T): T = mixins.add(mixin)
|
||||
protected fun <T: IMixin> mixin(cls: Class<out T>): T? = mixins.get(cls)
|
||||
|
||||
protected val runner: Runner
|
||||
get() = runnerDelegate.runner()
|
||||
|
||||
protected fun reloadPlaybackService() {
|
||||
if (!isPaused() && playbackService != null) {
|
||||
val playbackListener = playbackServiceEventListener
|
||||
|
||||
if (playbackListener != null) {
|
||||
playbackService?.disconnect(playbackServiceEventListener!!)
|
||||
}
|
||||
|
||||
playbackService = PlaybackServiceFactory.instance(this)
|
||||
|
||||
if (playbackListener != null) {
|
||||
playbackService?.connect(playbackServiceEventListener!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open val playbackServiceEventListener: (() -> Unit)?
|
||||
get() = null
|
||||
get() = mixin(RunnerMixin::class.java)!!.runner
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package io.casey.musikcube.remote.ui.shared.extension
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
import android.support.design.widget.Snackbar
|
||||
@ -19,6 +20,7 @@ import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.EditText
|
||||
import android.widget.TextView
|
||||
import io.casey.musikcube.remote.Application
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.ui.shared.activity.Filterable
|
||||
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
|
||||
@ -39,6 +41,10 @@ fun AppCompatActivity.setupDefaultRecyclerView(
|
||||
recyclerView.addItemDecoration(dividerItemDecoration)
|
||||
}
|
||||
|
||||
fun RecyclerView.ViewHolder.getColorCompat(resourceId: Int): Int {
|
||||
return ContextCompat.getColor(itemView.context, resourceId)
|
||||
}
|
||||
|
||||
fun View.getColorCompat(resourceId: Int): Int {
|
||||
return ContextCompat.getColor(context, resourceId)
|
||||
}
|
||||
@ -167,27 +173,37 @@ fun DialogFragment.hideKeyboard() {
|
||||
hideKeyboard(activity, activity.findViewById(android.R.id.content))
|
||||
}
|
||||
|
||||
fun AppCompatActivity.dialogVisible(tag: String): Boolean {
|
||||
return this.supportFragmentManager.findFragmentByTag(tag) != null
|
||||
}
|
||||
fun AppCompatActivity.dialogVisible(tag: String): Boolean =
|
||||
this.supportFragmentManager.findFragmentByTag(tag) != null
|
||||
|
||||
fun AppCompatActivity.showDialog(dialog: DialogFragment, tag: String) {
|
||||
dialog.show(this.supportFragmentManager, tag)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.showSnackbar(view: View, stringId: Int) {
|
||||
fun showSnackbar(view: View, stringId: Int, bgColor: Int, fgColor: Int) {
|
||||
val sb = Snackbar.make(view, stringId, Snackbar.LENGTH_LONG)
|
||||
val sbView = sb.view
|
||||
sbView.setBackgroundColor(getColorCompat(R.color.color_primary))
|
||||
val context = view.context
|
||||
sbView.setBackgroundColor(ContextCompat.getColor(context, bgColor))
|
||||
val tv = sbView.findViewById<TextView>(android.support.design.R.id.snackbar_text)
|
||||
tv.setTextColor(getColorCompat(R.color.theme_foreground))
|
||||
tv.setTextColor(ContextCompat.getColor(context, fgColor))
|
||||
sb.show()
|
||||
}
|
||||
|
||||
fun AppCompatActivity.showSnackbar(viewId: Int, stringId: Int) {
|
||||
this.showSnackbar(this.findViewById<View>(viewId), stringId)
|
||||
fun showSnackbar(view: View, stringId: Int) {
|
||||
showSnackbar(view, stringId, R.color.color_primary, R.color.theme_foreground)
|
||||
}
|
||||
|
||||
fun fallback(input: String?, fallback: String): String {
|
||||
return if (input.isNullOrEmpty()) fallback else input!!
|
||||
}
|
||||
fun showErrorSnackbar(view: View, stringId: Int) {
|
||||
showSnackbar(view, stringId, R.color.theme_red, R.color.theme_foreground)
|
||||
}
|
||||
|
||||
fun AppCompatActivity.showSnackbar(viewId: Int, stringId: Int) {
|
||||
showSnackbar(this.findViewById<View>(viewId), stringId)
|
||||
}
|
||||
|
||||
fun fallback(input: String?, fallback: String): String =
|
||||
if (input.isNullOrEmpty()) fallback else input!!
|
||||
|
||||
fun fallback(input: String?, fallback: Int): String =
|
||||
if (input.isNullOrEmpty()) Application.Companion.instance!!.getString(fallback) else input!!
|
||||
|
@ -0,0 +1,59 @@
|
||||
package io.casey.musikcube.remote.ui.shared.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import io.casey.musikcube.remote.framework.IMixin
|
||||
import io.casey.musikcube.remote.framework.MixinSet
|
||||
import io.casey.musikcube.remote.framework.ViewModel
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.ViewModelMixin
|
||||
|
||||
|
||||
open class BaseFragment: Fragment(), ViewModel.Provider {
|
||||
private val mixins = MixinSet()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
mixins.onCreate(savedInstanceState ?: Bundle())
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
mixins.onStart()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
mixins.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
mixins.onPause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
mixins.onStop()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
mixins.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle?) {
|
||||
super.onSaveInstanceState(outState)
|
||||
mixins.onSaveInstanceState(outState ?: Bundle())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
mixins.onDestroy()
|
||||
}
|
||||
|
||||
override fun <T: ViewModel<*>> createViewModel(): T? = null
|
||||
protected fun <T: ViewModel<*>> getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get<T>() as T
|
||||
protected fun <T: IMixin> mixin(mixin: T): T = mixins.add(mixin)
|
||||
protected fun <T: IMixin> mixin(cls: Class<out T>): T? = mixins.get(cls)
|
||||
}
|
@ -2,34 +2,33 @@ package io.casey.musikcube.remote.ui.shared.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
|
||||
import io.casey.musikcube.remote.ui.home.activity.MainActivity
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.playback.IPlaybackService
|
||||
import io.casey.musikcube.remote.service.playback.PlaybackServiceFactory
|
||||
import io.casey.musikcube.remote.service.playback.PlaybackState
|
||||
import io.casey.musikcube.remote.ui.home.activity.MainActivity
|
||||
import io.casey.musikcube.remote.ui.playqueue.activity.PlayQueueActivity
|
||||
import io.casey.musikcube.remote.ui.shared.extension.fallback
|
||||
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
|
||||
class TransportFragment : Fragment() {
|
||||
private var rootView: View? = null
|
||||
private var buffering: View? = null
|
||||
private var title: TextView? = null
|
||||
private var playPause: TextView? = null
|
||||
class TransportFragment: BaseFragment() {
|
||||
private lateinit var rootView: View
|
||||
private lateinit var buffering: View
|
||||
private lateinit var title: TextView
|
||||
private lateinit var playPause: TextView
|
||||
|
||||
lateinit var playback: PlaybackMixin
|
||||
private set
|
||||
|
||||
interface OnModelChangedListener {
|
||||
fun onChanged(fragment: TransportFragment)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater?,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View?
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater?, container: ViewGroup?, savedInstanceState: Bundle?): View?
|
||||
{
|
||||
this.rootView = inflater!!.inflate(R.layout.fragment_transport, container, false)
|
||||
bindEventHandlers()
|
||||
@ -38,83 +37,71 @@ class TransportFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
playback = mixin(PlaybackMixin(playbackListener))
|
||||
super.onCreate(savedInstanceState)
|
||||
this.playbackService = PlaybackServiceFactory.instance(activity)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
this.playbackService?.disconnect(playbackListener)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
rebindUi()
|
||||
this.playbackService?.connect(playbackListener)
|
||||
}
|
||||
|
||||
var playbackService: IPlaybackService? = null
|
||||
private set
|
||||
|
||||
var modelChangedListener: OnModelChangedListener? = null
|
||||
set(value) {
|
||||
field = value
|
||||
}
|
||||
|
||||
private fun bindEventHandlers() {
|
||||
this.title = this.rootView?.findViewById<TextView>(R.id.track_title)
|
||||
this.buffering = this.rootView?.findViewById<View>(R.id.buffering)
|
||||
this.title = this.rootView.findViewById(R.id.track_title)
|
||||
this.buffering = this.rootView.findViewById(R.id.buffering)
|
||||
|
||||
val titleBar = this.rootView?.findViewById<View>(R.id.title_bar)
|
||||
val titleBar = this.rootView.findViewById<View>(R.id.title_bar)
|
||||
|
||||
titleBar?.setOnClickListener { _: View ->
|
||||
if (playbackService?.playbackState != PlaybackState.Stopped) {
|
||||
if (playback.service.state != PlaybackState.Stopped) {
|
||||
val intent = PlayQueueActivity
|
||||
.getStartIntent(activity, playbackService?.queuePosition ?: 0)
|
||||
.getStartIntent(activity, playback.service.queuePosition)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
this.title?.setOnLongClickListener { _: View ->
|
||||
this.title.setOnLongClickListener { _: View ->
|
||||
startActivity(MainActivity.getStartIntent(activity))
|
||||
true
|
||||
}
|
||||
|
||||
this.rootView?.findViewById<View>(R.id.button_prev)?.setOnClickListener { _: View -> playbackService?.prev() }
|
||||
this.rootView.findViewById<View>(R.id.button_prev)?.setOnClickListener { _: View -> playback.service.prev() }
|
||||
|
||||
this.playPause = this.rootView?.findViewById<TextView>(R.id.button_play_pause)
|
||||
this.playPause = this.rootView.findViewById(R.id.button_play_pause)
|
||||
|
||||
this.playPause?.setOnClickListener { _: View ->
|
||||
if (playbackService?.playbackState == PlaybackState.Stopped) {
|
||||
playbackService?.playAll()
|
||||
this.playPause.setOnClickListener { _: View ->
|
||||
if (playback.service.state == PlaybackState.Stopped) {
|
||||
playback.service.playAll()
|
||||
}
|
||||
else {
|
||||
playbackService?.pauseOrResume()
|
||||
playback.service.pauseOrResume()
|
||||
}
|
||||
}
|
||||
|
||||
this.rootView?.findViewById<View>(R.id.button_next)?.setOnClickListener { _: View -> playbackService?.next() }
|
||||
this.rootView.findViewById<View>(R.id.button_next)?.setOnClickListener { _: View -> playback.service.next() }
|
||||
}
|
||||
|
||||
private fun rebindUi() {
|
||||
val state = playbackService?.playbackState
|
||||
val state = playback.service.state
|
||||
|
||||
val playing = state == PlaybackState.Playing
|
||||
val buffering = state == PlaybackState.Buffering
|
||||
|
||||
this.playPause?.setText(if (playing) R.string.button_pause else R.string.button_play)
|
||||
this.buffering?.visibility = if (buffering) View.VISIBLE else View.GONE
|
||||
this.playPause.setText(if (playing) R.string.button_pause else R.string.button_play)
|
||||
this.buffering.visibility = if (buffering) View.VISIBLE else View.GONE
|
||||
|
||||
if (state == PlaybackState.Stopped) {
|
||||
title?.setTextColor(getColorCompat(R.color.theme_disabled_foreground))
|
||||
title?.setText(R.string.transport_not_playing)
|
||||
title.setTextColor(getColorCompat(R.color.theme_disabled_foreground))
|
||||
title.setText(R.string.transport_not_playing)
|
||||
}
|
||||
else {
|
||||
val defaultValue = getString(if (buffering) R.string.buffering else R.string.unknown_title)
|
||||
title?.text = fallback(playbackService?.playingTrack?.title, defaultValue)
|
||||
title?.setTextColor(getColorCompat(R.color.theme_green))
|
||||
title.text = fallback(playback.service.playingTrack.title, defaultValue)
|
||||
title.setTextColor(getColorCompat(R.color.theme_green))
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,9 +112,6 @@ class TransportFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
val TAG = "TransportFragment"
|
||||
|
||||
fun newInstance(): TransportFragment {
|
||||
return TransportFragment()
|
||||
}
|
||||
fun newInstance(): TransportFragment = TransportFragment()
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
package io.casey.musikcube.remote.ui.shared.mixin
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import io.casey.musikcube.remote.Application
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.framework.MixinBase
|
||||
import io.casey.musikcube.remote.injection.DaggerViewComponent
|
||||
import io.casey.musikcube.remote.injection.DataModule
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.service.websocket.model.ITrack
|
||||
import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
|
||||
import io.casey.musikcube.remote.ui.category.activity.CategoryBrowseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.extension.showErrorSnackbar
|
||||
import io.casey.musikcube.remote.ui.shared.extension.showSnackbar
|
||||
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
import javax.inject.Inject
|
||||
|
||||
class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
|
||||
@Inject lateinit var provider: IDataProvider
|
||||
|
||||
private var pendingCode = -1
|
||||
private var completion: ((Long) -> Unit)? = null
|
||||
|
||||
init {
|
||||
DaggerViewComponent.builder()
|
||||
.appComponent(Application.appComponent)
|
||||
.dataModule(DataModule())
|
||||
.build()
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
provider.attach()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
provider.detach()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
provider.destroy()
|
||||
}
|
||||
|
||||
override fun onActivityResult(request: Int, result: Int, data: Intent?) {
|
||||
if (pendingCode == request) {
|
||||
if (result == Activity.RESULT_OK && data != null) {
|
||||
val playlistId = data.getLongExtra(CategoryBrowseActivity.EXTRA_ID, -1L)
|
||||
if (playlistId != -1L) {
|
||||
completion?.invoke(playlistId)
|
||||
}
|
||||
}
|
||||
pendingCode = -1
|
||||
completion = null
|
||||
}
|
||||
|
||||
super.onActivityResult(request, result, data)
|
||||
}
|
||||
|
||||
fun add(track: ITrack) {
|
||||
add(listOf(track))
|
||||
}
|
||||
|
||||
fun add(tracks: List<ITrack>) {
|
||||
showPlaylistChooser { id ->
|
||||
addWithErrorHandler(provider.appendToPlaylist(id, tracks))
|
||||
}
|
||||
}
|
||||
|
||||
fun add(categoryType: String, categoryId: Long) {
|
||||
showPlaylistChooser { id ->
|
||||
addWithErrorHandler(provider.appendToPlaylist(id, categoryType, categoryId))
|
||||
}
|
||||
}
|
||||
|
||||
fun add(category: ICategoryValue) {
|
||||
showPlaylistChooser { id ->
|
||||
addWithErrorHandler(provider.appendToPlaylist(id, category))
|
||||
}
|
||||
}
|
||||
|
||||
private fun addWithErrorHandler(observable: Observable<Boolean>) {
|
||||
val error = R.string.playlist_edit_add_error
|
||||
|
||||
observable.subscribeBy(
|
||||
onNext = { success -> if (success) showSuccess() else showError(error) },
|
||||
onError = { showError(error) })
|
||||
}
|
||||
|
||||
private fun showPlaylistChooser(callback: (Long) -> Unit) {
|
||||
completion = callback
|
||||
pendingCode = REQUEST_ADD_TO_PLAYLIST
|
||||
|
||||
val intent = CategoryBrowseActivity.getStartIntent(
|
||||
activity,
|
||||
Messages.Category.PLAYLISTS,
|
||||
CategoryBrowseActivity.NavigationType.Select)
|
||||
|
||||
activity.startActivityForResult(intent, pendingCode)
|
||||
}
|
||||
|
||||
fun showForTrack(track: ITrack, anchorView: View)
|
||||
{
|
||||
val popup = PopupMenu(activity, anchorView)
|
||||
popup.inflate(R.menu.item_context_menu)
|
||||
|
||||
popup.menu.removeItem(R.id.menu_show_tracks)
|
||||
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
val intent: Intent? = when (item.itemId) {
|
||||
R.id.menu_add_to_playlist -> {
|
||||
add(track)
|
||||
null
|
||||
}
|
||||
R.id.menu_show_albums -> {
|
||||
AlbumBrowseActivity.getStartIntent(
|
||||
activity, Messages.Category.ARTIST, track.artistId)
|
||||
}
|
||||
R.id.menu_show_artists -> {
|
||||
TrackListActivity.getStartIntent(
|
||||
activity,
|
||||
Messages.Category.ARTIST,
|
||||
track.artistId)
|
||||
}
|
||||
R.id.menu_show_genres -> {
|
||||
CategoryBrowseActivity.getStartIntent(
|
||||
activity,
|
||||
Messages.Category.GENRE,
|
||||
Messages.Category.ARTIST,
|
||||
track.artistId)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (intent != null) {
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
fun showForCategory(value: ICategoryValue, anchorView: View)
|
||||
{
|
||||
val popup = PopupMenu(activity, anchorView)
|
||||
popup.inflate(R.menu.item_context_menu)
|
||||
|
||||
if (value.type != Messages.Category.GENRE) {
|
||||
popup.menu.removeItem(R.id.menu_show_artists)
|
||||
}
|
||||
|
||||
when (value.type) {
|
||||
Messages.Category.ARTIST -> popup.menu.removeItem(R.id.menu_show_artists)
|
||||
Messages.Category.ALBUM -> popup.menu.removeItem(R.id.menu_show_albums)
|
||||
Messages.Category.GENRE -> popup.menu.removeItem(R.id.menu_show_genres)
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
val intent: Intent? = when (item.itemId) {
|
||||
R.id.menu_add_to_playlist -> {
|
||||
add(value)
|
||||
null
|
||||
}
|
||||
R.id.menu_show_albums -> {
|
||||
AlbumBrowseActivity.getStartIntent(activity, value.type, value.id)
|
||||
}
|
||||
R.id.menu_show_tracks -> {
|
||||
TrackListActivity.getStartIntent(activity, value.type, value.id)
|
||||
}
|
||||
R.id.menu_show_genres -> {
|
||||
CategoryBrowseActivity.getStartIntent(
|
||||
activity, Messages.Category.GENRE, value.type, value.id)
|
||||
}
|
||||
R.id.menu_show_artists -> {
|
||||
CategoryBrowseActivity.getStartIntent(
|
||||
activity, Messages.Category.ARTIST, value.type, value.id)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (intent != null) {
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
popup.show()
|
||||
}
|
||||
|
||||
private fun showSuccess() {
|
||||
showSnackbar(
|
||||
activity.findViewById(android.R.id.content),
|
||||
R.string.playlist_edit_add_success)
|
||||
}
|
||||
|
||||
private fun showError(message: Int) {
|
||||
showErrorSnackbar(activity.findViewById(android.R.id.content), message)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val REQUEST_ADD_TO_PLAYLIST = 128
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package io.casey.musikcube.remote.ui.shared.mixin
|
||||
|
||||
import android.os.Bundle
|
||||
import com.uacf.taskrunner.Runner
|
||||
import io.casey.musikcube.remote.framework.MixinBase
|
||||
|
||||
class RunnerMixin(private val callbacks: Runner.TaskCallbacks,
|
||||
private val callingType: Class<Any>): MixinBase()
|
||||
{
|
||||
lateinit var runner: Runner
|
||||
private set
|
||||
|
||||
override fun onCreate(bundle: Bundle) {
|
||||
super.onCreate(bundle)
|
||||
this.runner = Runner.attach(this.context, callingType, callbacks, bundle, null)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
runner.resume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
runner.pause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(bundle: Bundle) {
|
||||
super.onSaveInstanceState(bundle)
|
||||
runner.saveState(bundle)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
runner.detach(callbacks)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,48 @@
|
||||
package io.casey.musikcube.remote.ui.shared.mixin
|
||||
|
||||
import android.os.Bundle
|
||||
import io.casey.musikcube.remote.framework.MixinBase
|
||||
import io.casey.musikcube.remote.framework.ViewModel
|
||||
|
||||
class ViewModelMixin(private val provider: ViewModel.Provider): MixinBase() {
|
||||
private var viewModel: ViewModel<*>? = null
|
||||
|
||||
fun <T: ViewModel<*>> get(): T? = this.viewModel as T?
|
||||
|
||||
override fun onCreate(bundle: Bundle) {
|
||||
super.onCreate(bundle)
|
||||
|
||||
viewModel = ViewModel.restore(bundle.getLong(EXTRA_VIEW_MODEL_ID, -1))
|
||||
|
||||
if (viewModel == null) {
|
||||
viewModel = provider.createViewModel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
viewModel?.onResume()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
viewModel?.onPause()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(bundle: Bundle) {
|
||||
super.onSaveInstanceState(bundle)
|
||||
|
||||
if (viewModel != null) {
|
||||
bundle.putLong(EXTRA_VIEW_MODEL_ID, viewModel!!.id)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
viewModel?.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EXTRA_VIEW_MODEL_ID = "extra_view_model_id"
|
||||
}
|
||||
}
|
@ -3,41 +3,69 @@ package io.casey.musikcube.remote.ui.tracks.activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import com.simplecityapps.recyclerview_fastscroll.views.FastScrollRecyclerView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
|
||||
import io.casey.musikcube.remote.service.websocket.model.ITrack
|
||||
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.activity.Filterable
|
||||
import io.casey.musikcube.remote.ui.shared.constants.Navigation
|
||||
import io.casey.musikcube.remote.ui.shared.extension.*
|
||||
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
|
||||
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow.QueryFactory
|
||||
import io.casey.musikcube.remote.ui.shared.view.EmptyListView
|
||||
import io.casey.musikcube.remote.ui.shared.view.EmptyListView.Capability
|
||||
import io.casey.musikcube.remote.ui.tracks.adapter.TrackListAdapter
|
||||
import io.casey.musikcube.remote.util.Debouncer
|
||||
import io.casey.musikcube.remote.ui.shared.constants.Navigation
|
||||
import io.casey.musikcube.remote.util.Strings
|
||||
import io.casey.musikcube.remote.service.websocket.Messages
|
||||
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
|
||||
import io.casey.musikcube.remote.ui.shared.activity.Filterable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.rxkotlin.subscribeBy
|
||||
|
||||
class TrackListActivity : BaseActivity(), Filterable {
|
||||
private lateinit var tracks: TrackListSlidingWindow
|
||||
private lateinit var emptyView: EmptyListView
|
||||
private lateinit var transport: TransportFragment
|
||||
private lateinit var adapter: TrackListAdapter
|
||||
|
||||
private lateinit var data: DataProviderMixin
|
||||
private lateinit var playback: PlaybackMixin
|
||||
|
||||
private var categoryType: String = ""
|
||||
private var categoryId: Long = 0
|
||||
private var lastFilter = ""
|
||||
private var adapter = Adapter()
|
||||
|
||||
private val onItemClickListener = { view: View ->
|
||||
val index = view.tag as Int
|
||||
|
||||
if (isValidCategory(categoryType, categoryId)) {
|
||||
playback.service.play(categoryType, categoryId, index, lastFilter)
|
||||
}
|
||||
else {
|
||||
playback.service.playAll(index, lastFilter)
|
||||
}
|
||||
|
||||
setResult(Navigation.ResponseCode.PLAYBACK_STARTED)
|
||||
finish()
|
||||
}
|
||||
|
||||
private val onActionClickListener = { view: View ->
|
||||
val track = view.tag as ITrack
|
||||
mixin(ItemContextMenuMixin::class.java)?.showForTrack(track, view)
|
||||
Unit
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
component.inject(this)
|
||||
data = mixin(DataProviderMixin())
|
||||
playback = mixin(PlaybackMixin())
|
||||
mixin(ItemContextMenuMixin(this))
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@ -52,8 +80,11 @@ class TrackListActivity : BaseActivity(), Filterable {
|
||||
enableUpNavigation()
|
||||
|
||||
val queryFactory = createCategoryQueryFactory(categoryType, categoryId)
|
||||
|
||||
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
|
||||
|
||||
tracks = TrackListSlidingWindow(recyclerView, data.provider, queryFactory)
|
||||
adapter = TrackListAdapter(tracks, onItemClickListener, onActionClickListener, playback)
|
||||
|
||||
setupDefaultRecyclerView(recyclerView, adapter)
|
||||
|
||||
emptyView = findViewById(R.id.empty_list_view)
|
||||
@ -63,8 +94,6 @@ class TrackListActivity : BaseActivity(), Filterable {
|
||||
it.alternateView = recyclerView
|
||||
}
|
||||
|
||||
tracks = TrackListSlidingWindow(recyclerView, dataProvider, queryFactory)
|
||||
|
||||
tracks.setOnMetadataLoadedListener(slidingWindowListener)
|
||||
|
||||
transport = addTransportFragment(object: TransportFragment.OnModelChangedListener {
|
||||
@ -99,8 +128,8 @@ class TrackListActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
|
||||
private fun initObservers() {
|
||||
disposables.add(dataProvider.observeState().subscribe(
|
||||
{ states ->
|
||||
disposables.add(data.provider.observeState().subscribeBy(
|
||||
onNext = { states ->
|
||||
val shouldRequery =
|
||||
states.first === IDataProvider.State.Connected ||
|
||||
(states.first === IDataProvider.State.Disconnected && isOfflineTracks)
|
||||
@ -113,7 +142,8 @@ class TrackListActivity : BaseActivity(), Filterable {
|
||||
emptyView.update(states.first, adapter.itemCount)
|
||||
}
|
||||
},
|
||||
{ /* error */ }))
|
||||
onError = {
|
||||
}))
|
||||
}
|
||||
|
||||
private val filterDebouncer = object : Debouncer<String>(350) {
|
||||
@ -124,70 +154,6 @@ class TrackListActivity : BaseActivity(), Filterable {
|
||||
}
|
||||
}
|
||||
|
||||
private val onItemClickListener = { view: View ->
|
||||
val index = view.tag as Int
|
||||
|
||||
if (isValidCategory(categoryType, categoryId)) {
|
||||
playbackService?.play(categoryType, categoryId, index, lastFilter)
|
||||
}
|
||||
else {
|
||||
playbackService?.playAll(index, lastFilter)
|
||||
}
|
||||
|
||||
setResult(Navigation.ResponseCode.PLAYBACK_STARTED)
|
||||
finish()
|
||||
}
|
||||
|
||||
private inner class ViewHolder internal constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val title: TextView = itemView.findViewById(R.id.title)
|
||||
private val subtitle: TextView = itemView.findViewById(R.id.subtitle)
|
||||
|
||||
internal fun bind(track: ITrack?, position: Int) {
|
||||
itemView.tag = position
|
||||
|
||||
var titleColor = R.color.theme_foreground
|
||||
var subtitleColor = R.color.theme_disabled_foreground
|
||||
|
||||
if (track != null) {
|
||||
val playing = transport.playbackService!!.playingTrack
|
||||
val entryExternalId = track.externalId
|
||||
val playingExternalId = playing.externalId
|
||||
|
||||
if (entryExternalId == playingExternalId) {
|
||||
titleColor = R.color.theme_green
|
||||
subtitleColor = R.color.theme_yellow
|
||||
}
|
||||
|
||||
title.text = fallback(track.title, "-")
|
||||
subtitle.text = fallback(track.albumArtist, "-")
|
||||
}
|
||||
else {
|
||||
title.text = "-"
|
||||
subtitle.text = "-"
|
||||
}
|
||||
|
||||
title.setTextColor(getColorCompat(titleColor))
|
||||
subtitle.setTextColor(getColorCompat(subtitleColor))
|
||||
}
|
||||
}
|
||||
|
||||
private inner class Adapter : RecyclerView.Adapter<ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
|
||||
view.setOnClickListener(onItemClickListener)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(tracks.getTrack(position), position)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return tracks.count
|
||||
}
|
||||
}
|
||||
|
||||
private val emptyMessage: String
|
||||
get() {
|
||||
if (isOfflineTracks) {
|
||||
@ -210,48 +176,40 @@ class TrackListActivity : BaseActivity(), Filterable {
|
||||
if (isValidCategory(categoryType, categoryId)) {
|
||||
/* tracks for a specified category (album, artists, genres, etc */
|
||||
return object : QueryFactory() {
|
||||
override fun count(): Observable<Int> {
|
||||
return dataProvider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter)
|
||||
}
|
||||
override fun count(): Observable<Int> =
|
||||
data.provider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter)
|
||||
|
||||
override fun all(): Observable<List<ITrack>>? {
|
||||
return dataProvider.getTracksByCategory(categoryType ?: "", categoryId, lastFilter)
|
||||
}
|
||||
override fun all(): Observable<List<ITrack>>? =
|
||||
data.provider.getTracksByCategory(categoryType ?: "", categoryId, lastFilter)
|
||||
|
||||
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> {
|
||||
return dataProvider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter)
|
||||
}
|
||||
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> =
|
||||
data.provider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter)
|
||||
|
||||
override fun offline(): Boolean {
|
||||
return Messages.Category.OFFLINE == categoryType
|
||||
}
|
||||
override fun offline(): Boolean =
|
||||
Messages.Category.OFFLINE == categoryType
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* all tracks */
|
||||
return object : QueryFactory() {
|
||||
override fun count(): Observable<Int> {
|
||||
return dataProvider.getTrackCount(lastFilter)
|
||||
}
|
||||
override fun count(): Observable<Int> =
|
||||
data.provider.getTrackCount(lastFilter)
|
||||
|
||||
override fun all(): Observable<List<ITrack>>? {
|
||||
return dataProvider.getTracks(lastFilter)
|
||||
}
|
||||
override fun all(): Observable<List<ITrack>>? =
|
||||
data.provider.getTracks(lastFilter)
|
||||
|
||||
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> {
|
||||
return dataProvider.getTracks(limit, offset, lastFilter)
|
||||
}
|
||||
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> =
|
||||
data.provider.getTracks(limit, offset, lastFilter)
|
||||
|
||||
override fun offline(): Boolean {
|
||||
return Messages.Category.OFFLINE == categoryType
|
||||
}
|
||||
override fun offline(): Boolean =
|
||||
Messages.Category.OFFLINE == categoryType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val slidingWindowListener = object : TrackListSlidingWindow.OnMetadataLoadedListener {
|
||||
override fun onReloaded(count: Int) {
|
||||
emptyView.update(dataProvider.state, count)
|
||||
emptyView.update(data.provider.state, count)
|
||||
}
|
||||
|
||||
override fun onMetadataLoaded(offset: Int, count: Int) {}
|
||||
@ -285,12 +243,10 @@ class TrackListActivity : BaseActivity(), Filterable {
|
||||
return intent
|
||||
}
|
||||
|
||||
fun getStartIntent(context: Context): Intent {
|
||||
return Intent(context, TrackListActivity::class.java)
|
||||
}
|
||||
fun getStartIntent(context: Context): Intent =
|
||||
Intent(context, TrackListActivity::class.java)
|
||||
|
||||
private fun isValidCategory(categoryType: String?, categoryId: Long): Boolean {
|
||||
return categoryType != null && categoryType.isNotEmpty() && categoryId != -1L
|
||||
}
|
||||
private fun isValidCategory(categoryType: String?, categoryId: Long): Boolean =
|
||||
categoryType != null && categoryType.isNotEmpty() && categoryId != -1L
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
package io.casey.musikcube.remote.ui.tracks.adapter
|
||||
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import io.casey.musikcube.remote.R
|
||||
import io.casey.musikcube.remote.service.playback.IPlaybackService
|
||||
import io.casey.musikcube.remote.service.websocket.model.ITrack
|
||||
import io.casey.musikcube.remote.ui.shared.extension.fallback
|
||||
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
|
||||
import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin
|
||||
import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow
|
||||
|
||||
class TrackListAdapter(private val tracks: TrackListSlidingWindow,
|
||||
private val onItemClickListener: (View) -> Unit,
|
||||
private val onActionClickListener: (View) -> Unit,
|
||||
private var playback: PlaybackMixin) : RecyclerView.Adapter<TrackListAdapter.ViewHolder>()
|
||||
{
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackListAdapter.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val view = inflater.inflate(R.layout.simple_list_item, parent, false)
|
||||
view.setOnClickListener(onItemClickListener)
|
||||
view.findViewById<View>(R.id.action).setOnClickListener(onActionClickListener)
|
||||
return ViewHolder(view, playback)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(tracks.getTrack(position), position)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = tracks.count
|
||||
|
||||
class ViewHolder internal constructor(private val view: View,
|
||||
private val playback: PlaybackMixin) : RecyclerView.ViewHolder(view)
|
||||
{
|
||||
private val title: TextView = view.findViewById(R.id.title)
|
||||
private val subtitle: TextView = view.findViewById(R.id.subtitle)
|
||||
private val action: View = view.findViewById(R.id.action)
|
||||
|
||||
internal fun bind(track: ITrack?, position: Int) {
|
||||
itemView.tag = position
|
||||
action.tag = track
|
||||
|
||||
var titleColor = R.color.theme_foreground
|
||||
var subtitleColor = R.color.theme_disabled_foreground
|
||||
|
||||
if (track != null) {
|
||||
val playing = playback.service.playingTrack
|
||||
val entryExternalId = track.externalId
|
||||
val playingExternalId = playing.externalId
|
||||
|
||||
if (entryExternalId == playingExternalId) {
|
||||
titleColor = R.color.theme_green
|
||||
subtitleColor = R.color.theme_yellow
|
||||
}
|
||||
|
||||
title.text = fallback(track.title, "-")
|
||||
subtitle.text = fallback(track.albumArtist, "-")
|
||||
}
|
||||
else {
|
||||
title.text = "-"
|
||||
subtitle.text = "-"
|
||||
}
|
||||
|
||||
title.setTextColor(getColorCompat(titleColor))
|
||||
subtitle.setTextColor(getColorCompat(subtitleColor))
|
||||
}
|
||||
}
|
||||
}
|
23
src/musikdroid/app/src/main/res/drawable-v21/ic_overflow.xml
Normal file
23
src/musikdroid/app/src/main/res/drawable-v21/ic_overflow.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2014 The Android Open Source Project
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2c-1.1,0 -2,0.9 -2,2S10.9,8 12,8zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2c1.1,0 2,-0.9 2,-2S13.1,10 12,10zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2c1.1,0 2,-0.9 2,-2S13.1,16 12,16z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ripple
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="?attr/colorControlHighlight">
|
||||
<item android:drawable="@drawable/ic_overflow"/>
|
||||
</ripple>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/theme_button_background"/>
|
||||
</selector>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/theme_background"/>
|
||||
</selector>
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@color/theme_selected_background"/>
|
||||
</selector>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:drawable="@color/theme_button_background"/>
|
||||
</selector>
|
@ -1,17 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:minHeight="52dp"
|
||||
android:padding="8dp">
|
||||
android:orientation="horizontal"
|
||||
android:minHeight="52dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/artwork"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_weight="1.0"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical">
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
@ -22,7 +30,7 @@
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/theme_foreground"
|
||||
android:text="title"/>
|
||||
tools:text="title"/>
|
||||
|
||||
<TextView
|
||||
android:textSize="12dp"
|
||||
@ -34,8 +42,19 @@
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:textColor="@color/theme_disabled_foreground"
|
||||
android:text="subtitle"/>
|
||||
tools:text="subtitle"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
<ImageView
|
||||
android:id="@+id/action"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:src="@drawable/ic_overflow"
|
||||
android:padding="8dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center"/>
|
||||
|
||||
</LinearLayout>
|
23
src/musikdroid/app/src/main/res/menu/item_context_menu.xml
Normal file
23
src/musikdroid/app/src/main/res/menu/item_context_menu.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_add_to_playlist"
|
||||
android:title="@string/menu_add_to_playlist"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_show_artists"
|
||||
android:title="@string/menu_show_artists"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_show_albums"
|
||||
android:title="@string/menu_show_albums"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_show_genres"
|
||||
android:title="@string/menu_show_genres"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_show_tracks"
|
||||
android:title="@string/menu_show_tracks"/>
|
||||
</menu>
|
@ -63,6 +63,11 @@
|
||||
<string name="menu_playlists">playlists</string>
|
||||
<string name="menu_remote_toggle">remote playback</string>
|
||||
<string name="menu_offline_tracks">offline songs</string>
|
||||
<string name="menu_add_to_playlist">add to playlist</string>
|
||||
<string name="menu_show_tracks">songs</string>
|
||||
<string name="menu_show_albums">albums</string>
|
||||
<string name="menu_show_artists">artist</string>
|
||||
<string name="menu_show_genres">genres</string>
|
||||
<string name="unknown_value"><unknown></string>
|
||||
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
|
||||
<string name="snackbar_remote_enabled">switched to remote control mode</string>
|
||||
@ -110,4 +115,8 @@
|
||||
<string name="update_check_dialog_message">\nmusikbox version %s is now available. would you like to download it now?</string>
|
||||
<string name="update_check_dont_ask_again">don\'t ask me again for this version</string>
|
||||
<string name="buffering">buffering</string>
|
||||
<string name="playlist_edit_no_playlists">couldn\'t get playlists from server</string>
|
||||
<string name="playlist_edit_add_error">playlist update failed</string>
|
||||
<string name="playlist_edit_add_success">playlist updated</string>
|
||||
<string name="playlist_edit_list_title">pick a playlist</string>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user