diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 22599dbbb..b947b644e 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,7 +7,8 @@ musikcube: musikdroid: -* gapless playback! +* gapless playback (for supported media)! enable in settings > playback + engine > "ExoPlayer Gapless (experimental)" * album art is now displayed in album rows when browsing * context menus on most screens with the ability to switch between related content (e.g. albums by this artist, artists in this genre, etc) @@ -22,7 +23,6 @@ musikdroid: * updated Glide from v3 -> v4 * updated to Android Studio 3.0.1 and related tooling - sdk: * removed all Destroy() methods, standardized on Release() across the board diff --git a/src/musikdroid/README.md b/src/musikdroid/README.md index a6f67bbc6..2f8ddae1d 100644 --- a/src/musikdroid/README.md +++ b/src/musikdroid/README.md @@ -11,7 +11,7 @@ because `musikdroid` is not available in the Google Play store, it uses [fabric. this should allow you to build and test locally without special keys. TODO: simplify -the project is currently built using `Android Studio 3.0 Canary 6` +the project is currently built using `Android Studio 3.0.1` # attribution diff --git a/src/musikdroid/app/src/main/AndroidManifest.xml b/src/musikdroid/app/src/main/AndroidManifest.xml index ff667686f..a14d302c5 100644 --- a/src/musikdroid/app/src/main/AndroidManifest.xml +++ b/src/musikdroid/app/src/main/AndroidManifest.xml @@ -51,6 +51,11 @@ + + + (protected val runner: Runner? = null): Runner.TaskCallbacks { +abstract class ViewModel(protected val runner: Runner? = null): Runner.TaskCallbacks { val id: Long = nextId.incrementAndGet() + private val publisher by lazy { createSubject() } interface Provider { fun > createViewModel(): T? } - protected var listener: ListenerT? = null + protected var listener: T? = null private set - fun onPause() { + open fun onPause() { } - fun onResume() { + open fun onResume() { } - fun onDestroy() { + open fun onDestroy() { listener = null handler.postDelayed(cleanup, cleanupDelayMs) } - fun observe(listener: ListenerT) { - this.listener = listener + open fun onCleanup() { + + } + + fun observe(): Observable { + return publisher + .observeOn(AndroidSchedulers.mainThread()) + .subscribeOn(AndroidSchedulers.mainThread()) } val context: Context = Application.instance!! - internal val cleanup = object: Runnable { - override fun run() { - listener = null - idToInstance.remove(id) - } + internal val cleanup = Runnable { + listener = null + idToInstance.remove(id) + onCleanup() + } + + protected fun publish(value: T) { + publisher.onNext(value) + } + + open fun createSubject(): Subject { + return PublishSubject.create() } override fun onTaskError(name: String?, id: Long, task: Task<*, *>?, error: Throwable?) { diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt index 1681982a8..258de31a4 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/remote/RemotePlaybackService.kt @@ -373,7 +373,6 @@ class RemotePlaybackService : IPlaybackService { override val playlistQueryFactory: TrackListSlidingWindow.QueryFactory = object : TrackListSlidingWindow.QueryFactory() { override fun count(): Observable = dataProvider.getPlayQueueTracksCount() - override fun all(): Observable>? = dataProvider.getPlayQueueTracks() override fun page(offset: Int, limit: Int): Observable> = dataProvider.getPlayQueueTracks(limit, offset) override fun offline(): Boolean = false } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt index 94d3bcaa7..0795e9017 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/playback/impl/streaming/StreamingPlaybackService.kt @@ -774,20 +774,6 @@ class StreamingPlaybackService(context: Context) : IPlaybackService { return null } - override fun all(): Observable>? { - val params = params - if (params != null) { - if (Strings.notEmpty(params.category) && (params.categoryId >= 0)) { - return dataProvider.getTracksByCategory( - params.category ?: "", params.categoryId, params.filter) - } - else { - return dataProvider.getTracks(params.filter) - } - } - return null - } - override fun page(offset: Int, limit: Int): Observable>? { val params = params if (params != null) { diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt index a5f9a39e9..bf116b259 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/Messages.kt @@ -77,6 +77,7 @@ class Messages { val ID = "id" val COUNT = "count" val COUNT_ONLY = "count_only" + val IDS_ONLY = "ids_only" val OFFSET = "offset" val LIMIT = "limit" val INDEX = "index" diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/WebSocketService.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/WebSocketService.kt index 5c191ca42..cbf12357b 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/WebSocketService.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/WebSocketService.kt @@ -330,6 +330,8 @@ class WebSocketService constructor(private val context: Context) { subject.onError(ex) } + subject.doOnDispose { cancelMessage(mrd.id) } + if (!intercepted) { socket?.sendText(message.toString()) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt index ff48502e7..8f937e474 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/IDataProvider.kt @@ -22,6 +22,7 @@ interface IDataProvider { fun getTracks(externalIds: Set): Observable> fun getTrackCountByCategory(category: String, id: Long, filter: String = ""): Observable + fun getTrackIdsByCategory(category: String, id: Long, filter: String = ""): Observable> fun getTracksByCategory(category: String, id: Long, filter: String = ""): Observable> fun getTracksByCategory(category: String, id: Long, limit: Int, offset: Int, filter: String = ""): Observable> @@ -36,6 +37,7 @@ interface IDataProvider { fun createPlaylist(playlistName: String, categoryType: String = "", categoryId: Long = -1, filter: String = ""): Observable fun createPlaylist(playlistName: String, tracks: List = ArrayList()): Observable fun createPlaylistWithExternalIds(playlistName: String, externalIds: List = ArrayList()): Observable + fun overwritePlaylistWithExternalIds(playlistId: Long, externalIds: List = ArrayList()): Observable fun appendToPlaylist(playlistId: Long, categoryType: String = "", categoryId: Long = -1, filter: String = "", offset: Long = -1): Observable fun appendToPlaylist(playlistId: Long, tracks: List = ArrayList(), offset: Long = -1): Observable fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt index caea22f24..aa7e47f70 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/service/websocket/model/impl/remote/RemoteDataProvider.kt @@ -116,6 +116,21 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider .observeOn(AndroidSchedulers.mainThread()) } + override fun getTrackIdsByCategory(category: String, id: Long, filter: String): Observable> { + val message = SocketMessage.Builder + .request(Messages.Request.QueryTracksByCategory) + .addOption(Messages.Key.FILTER, filter) + .addOption(Messages.Key.CATEGORY, category) + .addOption(Messages.Key.ID, id) + .addOption(Messages.Key.COUNT_ONLY, false) + .addOption(Messages.Key.IDS_ONLY, true) + .build() + + return service.observe(message, client) + .flatMap> { socketMessage -> toStringList(socketMessage) } + .observeOn(AndroidSchedulers.mainThread()) + } + override fun getTracksByCategory(category: String, id: Long, filter: String): Observable> = getTracksByCategory(category, id, -1, -1, filter) @@ -241,7 +256,7 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider override fun createPlaylistWithExternalIds(playlistName: String, externalIds: List): Observable { if (playlistName.isBlank()) { - return Observable.just(0) + return Observable.just(-1L) } val jsonArray = JSONArray() @@ -258,6 +273,25 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider .observeOn(AndroidSchedulers.mainThread()) } + override fun overwritePlaylistWithExternalIds(playlistId: Long, externalIds: List): Observable { + if (playlistId < 0L) { + return Observable.just(-1L) + } + + val jsonArray = JSONArray() + externalIds.forEach { jsonArray.put(it) } + + val message = SocketMessage.Builder + .request(Messages.Request.SavePlaylist) + .addOption(Messages.Key.PLAYLIST_ID, playlistId) + .addOption(Messages.Key.EXTERNAL_IDS, jsonArray) + .build() + + return service.observe(message, client) + .flatMap { socketMessage -> extractId(socketMessage, Messages.Key.PLAYLIST_ID) } + .observeOn(AndroidSchedulers.mainThread()) + } + override fun appendToPlaylist(playlistId: Long, categoryType: String, categoryId: Long, filter: String, offset: Long): Observable { val message = SocketMessage.Builder .request(Messages.Request.AppendToPlaylist) @@ -461,6 +495,15 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider return Observable.just(albums) } + private fun toStringList(socketMessage: SocketMessage): Observable> { + val strings = ArrayList() + val json = socketMessage.getJsonArrayOption(Messages.Key.DATA, JSONArray())!! + for (i in 0 until json.length()) { + strings.add(json.getString(i)) + } + return Observable.just(strings) + } + private fun toCount(message: SocketMessage): Observable { return Observable.just(message.getIntOption(Messages.Key.COUNT, 0)) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt index 6da1ed5c7..7f714a76d 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/home/activity/MainActivity.kt @@ -410,30 +410,29 @@ class MainActivity : BaseActivity() { private fun scheduleUpdateTime(immediate: Boolean) { handler.removeCallbacks(updateTimeRunnable) handler.postDelayed(updateTimeRunnable, (if (immediate) 0 else 1000).toLong()) + handler.removeCallbacks(updateTimeRunnable) } - private val updateTimeRunnable = object: Runnable { - override fun run() { - val duration = playback.service.duration - val current: Double = if (seekbarValue == -1) playback.service.currentTime else seekbarValue.toDouble() + private val updateTimeRunnable = Runnable { + 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.service.bufferedTime.toInt() + currentTime.text = Duration.format(current) + totalTime.text = Duration.format(duration) + seekbar.max = duration.toInt() + seekbar.progress = current.toInt() + seekbar.secondaryProgress = playback.service.bufferedTime.toInt() - var currentTimeColor = R.color.theme_foreground - if (playback.service.state === PlaybackState.Paused) { - currentTimeColor = - if (++blink % 2 == 0) R.color.theme_foreground - else R.color.theme_blink_foreground - } - - currentTime.setTextColor(getColorCompat(currentTimeColor)) - - scheduleUpdateTime(false) + var currentTimeColor = R.color.theme_foreground + if (playback.service.state === PlaybackState.Paused) { + currentTimeColor = + if (++blink % 2 == 0) R.color.theme_foreground + else R.color.theme_blink_foreground } + + currentTime.setTextColor(getColorCompat(currentTimeColor)) + + scheduleUpdateTime(false) } private val muteListener = { _: CompoundButton, b: Boolean -> diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/adapter/PlayQueueAdapter.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/adapter/PlayQueueAdapter.kt index 43d96a48e..46aea10dd 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/adapter/PlayQueueAdapter.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/playqueue/adapter/PlayQueueAdapter.kt @@ -23,7 +23,7 @@ class PlayQueueAdapter(val tracks: TrackListSlidingWindow, override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val inflater = LayoutInflater.from(parent.context) - val view = inflater.inflate(R.layout.play_queue_row, parent, false) + val view = inflater.inflate(R.layout.playlist_track_row, parent, false) val action = view.findViewById(R.id.action) view.setOnClickListener{ v -> listener.onItemClicked(v.tag as Int) } action.setOnClickListener{ v -> listener.onActionClicked(v, v.tag as ITrack) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt index 21a04a3a5..ce9997b03 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/extension/Extensions.kt @@ -28,15 +28,12 @@ import io.casey.musikcube.remote.util.Strings val EXTRA_ACTIVITY_TITLE = "extra_title" fun AppCompatActivity.setupDefaultRecyclerView( - recyclerView: RecyclerView, - adapter: RecyclerView.Adapter<*>) { + recyclerView: RecyclerView, adapter: RecyclerView.Adapter<*>) +{ val layoutManager = LinearLayoutManager(this) - + val dividerItemDecoration = DividerItemDecoration(this, layoutManager.orientation) recyclerView.layoutManager = LinearLayoutManager(this) recyclerView.adapter = adapter - - val dividerItemDecoration = DividerItemDecoration(this, layoutManager.orientation) - recyclerView.addItemDecoration(dividerItemDecoration) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt index aa5677a99..b90aab000 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt @@ -28,6 +28,7 @@ import io.casey.musikcube.remote.ui.shared.extension.showErrorSnackbar import io.casey.musikcube.remote.ui.shared.extension.showKeyboard import io.casey.musikcube.remote.ui.shared.extension.showSnackbar import io.casey.musikcube.remote.ui.shared.fragment.BaseDialogFragment +import io.casey.musikcube.remote.ui.tracks.activity.EditPlaylistActivity import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy @@ -233,6 +234,9 @@ class ItemContextMenuMixin(private val activity: AppCompatActivity, R.id.menu_playlist_delete -> { ConfirmDeletePlaylistDialog.show(activity, this, playlistName, playlistId) } + R.id.menu_playlist_edit -> { + activity.startActivity(EditPlaylistActivity.getStartIntent(activity, playlistId)) + } R.id.menu_playlist_rename -> { EnterPlaylistNameDialog.showForRename(activity, this, playlistName, playlistId) } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ViewModelMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ViewModelMixin.kt index af463c662..108bfd06f 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ViewModelMixin.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ViewModelMixin.kt @@ -7,7 +7,12 @@ import io.casey.musikcube.remote.framework.ViewModel class ViewModelMixin(private val provider: ViewModel.Provider): MixinBase() { private var viewModel: ViewModel<*>? = null - fun > get(): T? = this.viewModel as T? + fun > get(): T? { + if (viewModel == null) { + viewModel = provider.createViewModel() + } + return viewModel as T? + } override fun onCreate(bundle: Bundle) { super.onCreate(bundle) diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/model/TrackListSlidingWindow.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/model/TrackListSlidingWindow.kt index 4c7fe98b2..3619c9fbb 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/model/TrackListSlidingWindow.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/model/TrackListSlidingWindow.kt @@ -40,7 +40,6 @@ class TrackListSlidingWindow(private val recyclerView: FastScrollRecyclerView, abstract class QueryFactory { abstract fun count(): Observable? - abstract fun all(): Observable>? abstract fun page(offset: Int, limit: Int): Observable>? abstract fun offline(): Boolean } diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/EditPlaylistActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/EditPlaylistActivity.kt new file mode 100644 index 000000000..d5b444c54 --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/EditPlaylistActivity.kt @@ -0,0 +1,106 @@ +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.support.v7.widget.helper.ItemTouchHelper +import android.view.Menu +import android.view.MenuItem +import io.casey.musikcube.remote.R +import io.casey.musikcube.remote.framework.ViewModel +import io.casey.musikcube.remote.ui.shared.activity.BaseActivity +import io.casey.musikcube.remote.ui.shared.extension.setupDefaultRecyclerView +import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin +import io.casey.musikcube.remote.ui.shared.mixin.ViewModelMixin +import io.casey.musikcube.remote.ui.tracks.adapter.EditPlaylistAdapter +import io.casey.musikcube.remote.ui.tracks.model.EditPlaylistViewModel +import io.casey.musikcube.remote.ui.tracks.model.EditPlaylistViewModel.Status +import io.reactivex.rxkotlin.subscribeBy + +class EditPlaylistActivity: BaseActivity() { + private lateinit var viewModel: EditPlaylistViewModel + private lateinit var data: DataProviderMixin + private lateinit var adapter: EditPlaylistAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + mixin(ViewModelMixin(this)) + data = mixin(DataProviderMixin()) + super.onCreate(savedInstanceState) + setContentView(R.layout.recycler_view_activity) + viewModel = getViewModel()!! + viewModel.attach(data.provider) + val recycler = findViewById(R.id.recycler_view) + val touchHelper = ItemTouchHelper(touchHelperCallback) + touchHelper.attachToRecyclerView(recycler) + adapter = EditPlaylistAdapter(viewModel, touchHelper) + setupDefaultRecyclerView(recycler, adapter) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.edit_playlist_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.action_save) { + viewModel.save().subscribeBy( + onNext = { playlistId -> + if (playlistId != -1L) { + finish() + } + else { + /* TODO ERROR SNACKBAR */ + } + }, + onError = { + /* TODO ERROR SNACKBAR */ + }) + } + return super.onOptionsItemSelected(item) + } + + override fun onResume() { + super.onResume() + + disposables.add(viewModel.observe().subscribeBy( + onNext = { status -> + if (status == Status.Updated) { + adapter.notifyDataSetChanged() + } + }, + onError = { } + )) + } + + override fun > createViewModel(): T? { + return EditPlaylistViewModel(intent.extras.getLong(EXTRA_PLAYLIST_ID, -1L)) as T + } + + private val touchHelperCallback = object:ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP or ItemTouchHelper.DOWN, + ItemTouchHelper.LEFT) + { + override fun onMove(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { + val from = viewHolder.adapterPosition + val to = target.adapterPosition + viewModel.move(from, to) + adapter.notifyItemMoved(from, to) + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + viewModel.remove(viewHolder.adapterPosition) + adapter.notifyItemRemoved(viewHolder.adapterPosition) + } + } + + companion object { + private val EXTRA_PLAYLIST_ID = "extra_playlist_id" + + fun getStartIntent(context: Context, playlistId: Long): Intent { + return Intent(context, EditPlaylistActivity::class.java) + .putExtra(EXTRA_PLAYLIST_ID, playlistId) + } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt index a4093e1c9..b6f5af15e 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt @@ -185,9 +185,6 @@ class TrackListActivity : BaseActivity(), Filterable { override fun count(): Observable = data.provider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter) - override fun all(): Observable>? = - data.provider.getTracksByCategory(categoryType ?: "", categoryId, lastFilter) - override fun page(offset: Int, limit: Int): Observable> = data.provider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter) @@ -201,9 +198,6 @@ class TrackListActivity : BaseActivity(), Filterable { override fun count(): Observable = data.provider.getTrackCount(lastFilter) - override fun all(): Observable>? = - data.provider.getTracks(lastFilter) - override fun page(offset: Int, limit: Int): Observable> = data.provider.getTracks(limit, offset, lastFilter) diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/EditPlaylistAdapter.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/EditPlaylistAdapter.kt new file mode 100644 index 000000000..7a2941fee --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/EditPlaylistAdapter.kt @@ -0,0 +1,70 @@ +package io.casey.musikcube.remote.ui.tracks.adapter + +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.helper.ItemTouchHelper +import android.view.LayoutInflater +import android.view.MotionEvent +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.model.ITrack +import io.casey.musikcube.remote.ui.shared.extension.fallback +import io.casey.musikcube.remote.ui.tracks.model.EditPlaylistViewModel + +class EditPlaylistAdapter(private val viewModel: EditPlaylistViewModel, + private val touchHelper: ItemTouchHelper): RecyclerView.Adapter() { + override fun getItemCount(): Int { + return viewModel.count + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = inflater.inflate(R.layout.edit_playlist_track_row, parent, false) + val holder = ViewHolder(view) + val drag = view.findViewById(R.id.dragHandle) + val swipe = view.findViewById(R.id.swipeHandle) + view.setOnClickListener(emptyClickListener) + drag.setOnTouchListener(dragTouchHandler) + swipe.setOnTouchListener(dragTouchHandler) + drag.tag = holder + swipe.tag = holder + return holder + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bind(viewModel[position]) + } + + private val emptyClickListener = object: View.OnClickListener { + override fun onClick(view: View?) { + /* we do this so we get a ripple effect when the user touches the view, + so there's an indication something is happening before the drag starts */ + } + } + + private val dragTouchHandler = object: View.OnTouchListener { + override fun onTouch(view: View, event: MotionEvent?): Boolean { + if (event?.actionMasked == MotionEvent.ACTION_DOWN) { + if (view.id == R.id.dragHandle) { + touchHelper.startDrag(view.tag as RecyclerView.ViewHolder) + } + else if (view.id == R.id.swipeHandle) { + touchHelper.startSwipe(view.tag as RecyclerView.ViewHolder) + return true + } + } + return false + } + } + + class ViewHolder internal constructor(internal val view: View) : RecyclerView.ViewHolder(view) { + private val title = itemView.findViewById(R.id.title) + private val subtitle = itemView.findViewById(R.id.subtitle) + + fun bind(track: ITrack) { + title.text = fallback(track.title, "-") + subtitle.text = fallback(track.albumArtist, "-") + } + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/model/EditPlaylistViewModel.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/model/EditPlaylistViewModel.kt new file mode 100644 index 000000000..782ee094d --- /dev/null +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/model/EditPlaylistViewModel.kt @@ -0,0 +1,142 @@ +package io.casey.musikcube.remote.ui.tracks.model + +import io.casey.musikcube.remote.framework.ViewModel +import io.casey.musikcube.remote.service.websocket.Messages.Category.Companion.PLAYLISTS +import io.casey.musikcube.remote.service.websocket.model.IDataProvider +import io.casey.musikcube.remote.service.websocket.model.ITrack +import io.casey.musikcube.remote.service.websocket.model.impl.remote.RemoteTrack +import io.reactivex.Observable +import io.reactivex.disposables.Disposable +import io.reactivex.rxkotlin.subscribeBy +import org.json.JSONObject + +class EditPlaylistViewModel(private val playlistId: Long): ViewModel() { + enum class Status { NotLoaded, Error, Loading, Saving, Updated } + + private data class CacheEntry(var track: ITrack, var dirty: Boolean = false) + + private var metadataDisposable: Disposable? = null + private var requestOffset = -1 + private var dataProvider: IDataProvider? = null + private var externalIds: MutableList = mutableListOf() + + private val cache = object : LinkedHashMap() { + override fun removeEldestEntry(eldest: MutableMap.MutableEntry): Boolean = size >= MAX_SIZE + } + + var modified: Boolean = false + private set(value) { + field = value + } + + fun attach(dataProvider: IDataProvider) { + this.dataProvider = dataProvider + } + + override fun onDestroy() { + super.onDestroy() + this.dataProvider = null + } + + var status: Status = Status.NotLoaded + private set(value) { + field = value + publish(value) + } + + val count: Int + get() { + if (externalIds.isEmpty() && status != Status.Loading) { + refreshTrackIds() + } + return externalIds.size + } + + operator fun get(index: Int): ITrack { + val entry = cache[externalIds[index]] + if (entry == null) { + refreshPageAround(index) + } + return entry?.track ?: DEFAULT_TRACK + } + + fun save(): Observable { + if (!modified) { + return Observable.just(playlistId) + } + + return dataProvider?.overwritePlaylistWithExternalIds(playlistId, externalIds.toList()) ?: Observable.just(-1L) + } + + fun remove(index: Int) { + externalIds.removeAt(index) + } + + fun move(from: Int, to: Int) { + val id = externalIds.removeAt(from) + externalIds.add(if (to > from) (to - 1) else to, id) + } + + private fun refreshTrackIds() { + status = Status.Loading + dataProvider?.let { + status = Status.Loading + + it.getTrackIdsByCategory(PLAYLISTS, playlistId).subscribeBy( + onNext = { result -> + externalIds = result.toMutableList() + status = Status.Updated + }, + onError = { + status = Status.Error + }) + } + } + + private fun refreshPageAround(offset: Int) { + if (requestOffset != -1 && offset >= requestOffset && offset < requestOffset + PAGE_SIZE) { + return /* in flight */ + } + + dataProvider?.let { + metadataDisposable?.dispose() + metadataDisposable = null + + requestOffset = Math.max(0, offset - PAGE_SIZE / 4) + val end = Math.min(externalIds.size, requestOffset + PAGE_SIZE) + val ids = mutableSetOf() + for (i in requestOffset until end) { + val id = externalIds[i] + val entry = cache[id] + if (entry == null || entry.dirty) { + ids.add(id) + } + } + + if (ids.isNotEmpty()) { + status = Status.Loading + + metadataDisposable = it.getTracks(ids) + .flatMapIterable { list: Map -> list.asIterable() } + .subscribeBy( + onNext = { entry: Map.Entry -> + cache.put(entry.key, CacheEntry(entry.value)) + }, + onError = { + status = Status.Error + requestOffset = -1 + }, + onComplete = { + status = Status.Updated + requestOffset = -1 + }) + } + } + } + + companion object { + private val DEFAULT_TRACK = RemoteTrack(JSONObject()) + private val PAGE_SIZE = 40 + private val MAX_SIZE = 150 + } +} \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/drawable-v21/ic_leftarrow.xml b/src/musikdroid/app/src/main/res/drawable-v21/ic_leftarrow.xml new file mode 100644 index 000000000..926c1394e --- /dev/null +++ b/src/musikdroid/app/src/main/res/drawable-v21/ic_leftarrow.xml @@ -0,0 +1,4 @@ + + + diff --git a/src/musikdroid/app/src/main/res/drawable-v21/ic_reorder.xml b/src/musikdroid/app/src/main/res/drawable-v21/ic_reorder.xml new file mode 100644 index 000000000..d95a2e5d1 --- /dev/null +++ b/src/musikdroid/app/src/main/res/drawable-v21/ic_reorder.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/musikdroid/app/src/main/res/drawable-v21/ic_trashcan.xml b/src/musikdroid/app/src/main/res/drawable-v21/ic_trashcan.xml new file mode 100644 index 000000000..b8b794e30 --- /dev/null +++ b/src/musikdroid/app/src/main/res/drawable-v21/ic_trashcan.xml @@ -0,0 +1,9 @@ + + + diff --git a/src/musikdroid/app/src/main/res/drawable-v21/opaque_row_background.xml b/src/musikdroid/app/src/main/res/drawable-v21/opaque_row_background.xml new file mode 100644 index 000000000..a5db07c7c --- /dev/null +++ b/src/musikdroid/app/src/main/res/drawable-v21/opaque_row_background.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/layout/edit_playlist_track_row.xml b/src/musikdroid/app/src/main/res/layout/edit_playlist_track_row.xml new file mode 100644 index 000000000..1ae89d650 --- /dev/null +++ b/src/musikdroid/app/src/main/res/layout/edit_playlist_track_row.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/layout/play_queue_row.xml b/src/musikdroid/app/src/main/res/layout/playlist_track_row.xml similarity index 100% rename from src/musikdroid/app/src/main/res/layout/play_queue_row.xml rename to src/musikdroid/app/src/main/res/layout/playlist_track_row.xml diff --git a/src/musikdroid/app/src/main/res/menu/edit_playlist_menu.xml b/src/musikdroid/app/src/main/res/menu/edit_playlist_menu.xml new file mode 100644 index 000000000..c3f527672 --- /dev/null +++ b/src/musikdroid/app/src/main/res/menu/edit_playlist_menu.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/src/musikdroid/app/src/main/res/menu/playlist_item_context_menu.xml b/src/musikdroid/app/src/main/res/menu/playlist_item_context_menu.xml index 662a329cb..dd85d8efd 100644 --- a/src/musikdroid/app/src/main/res/menu/playlist_item_context_menu.xml +++ b/src/musikdroid/app/src/main/res/menu/playlist_item_context_menu.xml @@ -4,6 +4,10 @@ android:id="@+id/menu_playlist_play" android:title="@string/menu_play_playlist"/> + + diff --git a/src/musikdroid/app/src/main/res/values/strings.xml b/src/musikdroid/app/src/main/res/values/strings.xml index faef186e1..0a192bc61 100644 --- a/src/musikdroid/app/src/main/res/values/strings.xml +++ b/src/musikdroid/app/src/main/res/values/strings.xml @@ -78,6 +78,7 @@ genres delete rename + edit play now <unknown> switched to streaming mode