mirror of
https://github.com/clangen/musikcube.git
synced 2024-10-02 04:52:32 +00:00
Incremental work to support playlist editing.
This commit is contained in:
parent
fc99911ad1
commit
78c57b8f10
@ -7,7 +7,8 @@ musikcube:
|
|||||||
|
|
||||||
musikdroid:
|
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
|
* album art is now displayed in album rows when browsing
|
||||||
* context menus on most screens with the ability to switch between related
|
* context menus on most screens with the ability to switch between related
|
||||||
content (e.g. albums by this artist, artists in this genre, etc)
|
content (e.g. albums by this artist, artists in this genre, etc)
|
||||||
@ -22,7 +23,6 @@ musikdroid:
|
|||||||
* updated Glide from v3 -> v4
|
* updated Glide from v3 -> v4
|
||||||
* updated to Android Studio 3.0.1 and related tooling
|
* updated to Android Studio 3.0.1 and related tooling
|
||||||
|
|
||||||
|
|
||||||
sdk:
|
sdk:
|
||||||
|
|
||||||
* removed all Destroy() methods, standardized on Release() across the board
|
* removed all Destroy() methods, standardized on Release() across the board
|
||||||
|
@ -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
|
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
|
# attribution
|
||||||
|
|
||||||
|
@ -51,6 +51,11 @@
|
|||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".ui.tracks.activity.EditPlaylistActivity"
|
||||||
|
android:screenOrientation="portrait"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="io.casey.musikcube.remote.ui.category.activity.CategoryBrowseActivity"
|
android:name="io.casey.musikcube.remote.ui.category.activity.CategoryBrowseActivity"
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
|
@ -6,40 +6,58 @@ import android.os.Looper
|
|||||||
import com.uacf.taskrunner.Runner
|
import com.uacf.taskrunner.Runner
|
||||||
import com.uacf.taskrunner.Task
|
import com.uacf.taskrunner.Task
|
||||||
import io.casey.musikcube.remote.Application
|
import io.casey.musikcube.remote.Application
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.subjects.PublishSubject
|
||||||
|
import io.reactivex.subjects.Subject
|
||||||
import java.util.concurrent.atomic.AtomicLong
|
import java.util.concurrent.atomic.AtomicLong
|
||||||
|
|
||||||
abstract class ViewModel<ListenerT>(protected val runner: Runner? = null): Runner.TaskCallbacks {
|
abstract class ViewModel<T>(protected val runner: Runner? = null): Runner.TaskCallbacks {
|
||||||
val id: Long = nextId.incrementAndGet()
|
val id: Long = nextId.incrementAndGet()
|
||||||
|
private val publisher by lazy { createSubject() }
|
||||||
|
|
||||||
interface Provider {
|
interface Provider {
|
||||||
fun <T: ViewModel<*>> createViewModel(): T?
|
fun <T: ViewModel<*>> createViewModel(): T?
|
||||||
}
|
}
|
||||||
|
|
||||||
protected var listener: ListenerT? = null
|
protected var listener: T? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
fun onPause() {
|
open fun onPause() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onResume() {
|
open fun onResume() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDestroy() {
|
open fun onDestroy() {
|
||||||
listener = null
|
listener = null
|
||||||
handler.postDelayed(cleanup, cleanupDelayMs)
|
handler.postDelayed(cleanup, cleanupDelayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observe(listener: ListenerT) {
|
open fun onCleanup() {
|
||||||
this.listener = listener
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun observe(): Observable<T> {
|
||||||
|
return publisher
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|
||||||
val context: Context = Application.instance!!
|
val context: Context = Application.instance!!
|
||||||
|
|
||||||
internal val cleanup = object: Runnable {
|
internal val cleanup = Runnable {
|
||||||
override fun run() {
|
|
||||||
listener = null
|
listener = null
|
||||||
idToInstance.remove(id)
|
idToInstance.remove(id)
|
||||||
|
onCleanup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected fun publish(value: T) {
|
||||||
|
publisher.onNext(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun createSubject(): Subject<T> {
|
||||||
|
return PublishSubject.create<T>()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTaskError(name: String?, id: Long, task: Task<*, *>?, error: Throwable?) {
|
override fun onTaskError(name: String?, id: Long, task: Task<*, *>?, error: Throwable?) {
|
||||||
|
@ -373,7 +373,6 @@ class RemotePlaybackService : IPlaybackService {
|
|||||||
|
|
||||||
override val playlistQueryFactory: TrackListSlidingWindow.QueryFactory = object : TrackListSlidingWindow.QueryFactory() {
|
override val playlistQueryFactory: TrackListSlidingWindow.QueryFactory = object : TrackListSlidingWindow.QueryFactory() {
|
||||||
override fun count(): Observable<Int> = dataProvider.getPlayQueueTracksCount()
|
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 page(offset: Int, limit: Int): Observable<List<ITrack>> = dataProvider.getPlayQueueTracks(limit, offset)
|
||||||
override fun offline(): Boolean = false
|
override fun offline(): Boolean = false
|
||||||
}
|
}
|
||||||
|
@ -774,20 +774,6 @@ class StreamingPlaybackService(context: Context) : IPlaybackService {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun all(): Observable<List<ITrack>>? {
|
|
||||||
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<List<ITrack>>? {
|
override fun page(offset: Int, limit: Int): Observable<List<ITrack>>? {
|
||||||
val params = params
|
val params = params
|
||||||
if (params != null) {
|
if (params != null) {
|
||||||
|
@ -77,6 +77,7 @@ class Messages {
|
|||||||
val ID = "id"
|
val ID = "id"
|
||||||
val COUNT = "count"
|
val COUNT = "count"
|
||||||
val COUNT_ONLY = "count_only"
|
val COUNT_ONLY = "count_only"
|
||||||
|
val IDS_ONLY = "ids_only"
|
||||||
val OFFSET = "offset"
|
val OFFSET = "offset"
|
||||||
val LIMIT = "limit"
|
val LIMIT = "limit"
|
||||||
val INDEX = "index"
|
val INDEX = "index"
|
||||||
|
@ -330,6 +330,8 @@ class WebSocketService constructor(private val context: Context) {
|
|||||||
subject.onError(ex)
|
subject.onError(ex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subject.doOnDispose { cancelMessage(mrd.id) }
|
||||||
|
|
||||||
if (!intercepted) {
|
if (!intercepted) {
|
||||||
socket?.sendText(message.toString())
|
socket?.sendText(message.toString())
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ interface IDataProvider {
|
|||||||
fun getTracks(externalIds: Set<String>): Observable<Map<String, ITrack>>
|
fun getTracks(externalIds: Set<String>): Observable<Map<String, ITrack>>
|
||||||
|
|
||||||
fun getTrackCountByCategory(category: String, id: Long, filter: String = ""): Observable<Int>
|
fun getTrackCountByCategory(category: String, id: Long, filter: String = ""): Observable<Int>
|
||||||
|
fun getTrackIdsByCategory(category: String, id: Long, filter: String = ""): Observable<List<String>>
|
||||||
fun getTracksByCategory(category: String, id: Long, filter: String = ""): Observable<List<ITrack>>
|
fun getTracksByCategory(category: String, id: Long, filter: String = ""): Observable<List<ITrack>>
|
||||||
fun getTracksByCategory(category: String, id: Long, limit: Int, offset: Int, filter: String = ""): Observable<List<ITrack>>
|
fun getTracksByCategory(category: String, id: Long, limit: Int, offset: Int, filter: String = ""): Observable<List<ITrack>>
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ interface IDataProvider {
|
|||||||
fun createPlaylist(playlistName: String, categoryType: String = "", categoryId: Long = -1, filter: String = ""): Observable<Long>
|
fun createPlaylist(playlistName: String, categoryType: String = "", categoryId: Long = -1, filter: String = ""): Observable<Long>
|
||||||
fun createPlaylist(playlistName: String, tracks: List<ITrack> = ArrayList()): Observable<Long>
|
fun createPlaylist(playlistName: String, tracks: List<ITrack> = ArrayList()): Observable<Long>
|
||||||
fun createPlaylistWithExternalIds(playlistName: String, externalIds: List<String> = ArrayList()): Observable<Long>
|
fun createPlaylistWithExternalIds(playlistName: String, externalIds: List<String> = ArrayList()): Observable<Long>
|
||||||
|
fun overwritePlaylistWithExternalIds(playlistId: Long, 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, 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, tracks: List<ITrack> = ArrayList(), offset: Long = -1): Observable<Boolean>
|
||||||
fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable<Boolean>
|
fun appendToPlaylist(playlistId: Long, categoryValue: ICategoryValue): Observable<Boolean>
|
||||||
|
@ -116,6 +116,21 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getTrackIdsByCategory(category: String, id: Long, filter: String): Observable<List<String>> {
|
||||||
|
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<List<String>> { socketMessage -> toStringList(socketMessage) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
override fun getTracksByCategory(category: String, id: Long, filter: String): Observable<List<ITrack>> =
|
override fun getTracksByCategory(category: String, id: Long, filter: String): Observable<List<ITrack>> =
|
||||||
getTracksByCategory(category, id, -1, -1, filter)
|
getTracksByCategory(category, id, -1, -1, filter)
|
||||||
|
|
||||||
@ -241,7 +256,7 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
|
|||||||
|
|
||||||
override fun createPlaylistWithExternalIds(playlistName: String, externalIds: List<String>): Observable<Long> {
|
override fun createPlaylistWithExternalIds(playlistName: String, externalIds: List<String>): Observable<Long> {
|
||||||
if (playlistName.isBlank()) {
|
if (playlistName.isBlank()) {
|
||||||
return Observable.just(0)
|
return Observable.just(-1L)
|
||||||
}
|
}
|
||||||
|
|
||||||
val jsonArray = JSONArray()
|
val jsonArray = JSONArray()
|
||||||
@ -258,6 +273,25 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
|
|||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun overwritePlaylistWithExternalIds(playlistId: Long, externalIds: List<String>): Observable<Long> {
|
||||||
|
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<Long> { socketMessage -> extractId(socketMessage, Messages.Key.PLAYLIST_ID) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
override fun appendToPlaylist(playlistId: Long, categoryType: String, categoryId: Long, filter: String, offset: Long): Observable<Boolean> {
|
override fun appendToPlaylist(playlistId: Long, categoryType: String, categoryId: Long, filter: String, offset: Long): Observable<Boolean> {
|
||||||
val message = SocketMessage.Builder
|
val message = SocketMessage.Builder
|
||||||
.request(Messages.Request.AppendToPlaylist)
|
.request(Messages.Request.AppendToPlaylist)
|
||||||
@ -461,6 +495,15 @@ class RemoteDataProvider(private val service: WebSocketService) : IDataProvider
|
|||||||
return Observable.just(albums)
|
return Observable.just(albums)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun toStringList(socketMessage: SocketMessage): Observable<List<String>> {
|
||||||
|
val strings = ArrayList<String>()
|
||||||
|
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<Int> {
|
private fun toCount(message: SocketMessage): Observable<Int> {
|
||||||
return Observable.just(message.getIntOption(Messages.Key.COUNT, 0))
|
return Observable.just(message.getIntOption(Messages.Key.COUNT, 0))
|
||||||
}
|
}
|
||||||
|
@ -410,10 +410,10 @@ class MainActivity : BaseActivity() {
|
|||||||
private fun scheduleUpdateTime(immediate: Boolean) {
|
private fun scheduleUpdateTime(immediate: Boolean) {
|
||||||
handler.removeCallbacks(updateTimeRunnable)
|
handler.removeCallbacks(updateTimeRunnable)
|
||||||
handler.postDelayed(updateTimeRunnable, (if (immediate) 0 else 1000).toLong())
|
handler.postDelayed(updateTimeRunnable, (if (immediate) 0 else 1000).toLong())
|
||||||
|
handler.removeCallbacks(updateTimeRunnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val updateTimeRunnable = object: Runnable {
|
private val updateTimeRunnable = Runnable {
|
||||||
override fun run() {
|
|
||||||
val duration = playback.service.duration
|
val duration = playback.service.duration
|
||||||
val current: Double = if (seekbarValue == -1) playback.service.currentTime else seekbarValue.toDouble()
|
val current: Double = if (seekbarValue == -1) playback.service.currentTime else seekbarValue.toDouble()
|
||||||
|
|
||||||
@ -434,7 +434,6 @@ class MainActivity : BaseActivity() {
|
|||||||
|
|
||||||
scheduleUpdateTime(false)
|
scheduleUpdateTime(false)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private val muteListener = { _: CompoundButton, b: Boolean ->
|
private val muteListener = { _: CompoundButton, b: Boolean ->
|
||||||
if (b != playback.service.muted) {
|
if (b != playback.service.muted) {
|
||||||
|
@ -23,7 +23,7 @@ class PlayQueueAdapter(val tracks: TrackListSlidingWindow,
|
|||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
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<View>(R.id.action)
|
val action = view.findViewById<View>(R.id.action)
|
||||||
view.setOnClickListener{ v -> listener.onItemClicked(v.tag as Int) }
|
view.setOnClickListener{ v -> listener.onItemClicked(v.tag as Int) }
|
||||||
action.setOnClickListener{ v -> listener.onActionClicked(v, v.tag as ITrack) }
|
action.setOnClickListener{ v -> listener.onActionClicked(v, v.tag as ITrack) }
|
||||||
|
@ -28,15 +28,12 @@ import io.casey.musikcube.remote.util.Strings
|
|||||||
val EXTRA_ACTIVITY_TITLE = "extra_title"
|
val EXTRA_ACTIVITY_TITLE = "extra_title"
|
||||||
|
|
||||||
fun AppCompatActivity.setupDefaultRecyclerView(
|
fun AppCompatActivity.setupDefaultRecyclerView(
|
||||||
recyclerView: RecyclerView,
|
recyclerView: RecyclerView, adapter: RecyclerView.Adapter<*>)
|
||||||
adapter: RecyclerView.Adapter<*>) {
|
{
|
||||||
val layoutManager = LinearLayoutManager(this)
|
val layoutManager = LinearLayoutManager(this)
|
||||||
|
val dividerItemDecoration = DividerItemDecoration(this, layoutManager.orientation)
|
||||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
recyclerView.adapter = adapter
|
recyclerView.adapter = adapter
|
||||||
|
|
||||||
val dividerItemDecoration = DividerItemDecoration(this, layoutManager.orientation)
|
|
||||||
|
|
||||||
recyclerView.addItemDecoration(dividerItemDecoration)
|
recyclerView.addItemDecoration(dividerItemDecoration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.showKeyboard
|
||||||
import io.casey.musikcube.remote.ui.shared.extension.showSnackbar
|
import io.casey.musikcube.remote.ui.shared.extension.showSnackbar
|
||||||
import io.casey.musikcube.remote.ui.shared.fragment.BaseDialogFragment
|
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.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.rxkotlin.subscribeBy
|
import io.reactivex.rxkotlin.subscribeBy
|
||||||
@ -233,6 +234,9 @@ class ItemContextMenuMixin(private val activity: AppCompatActivity,
|
|||||||
R.id.menu_playlist_delete -> {
|
R.id.menu_playlist_delete -> {
|
||||||
ConfirmDeletePlaylistDialog.show(activity, this, playlistName, playlistId)
|
ConfirmDeletePlaylistDialog.show(activity, this, playlistName, playlistId)
|
||||||
}
|
}
|
||||||
|
R.id.menu_playlist_edit -> {
|
||||||
|
activity.startActivity(EditPlaylistActivity.getStartIntent(activity, playlistId))
|
||||||
|
}
|
||||||
R.id.menu_playlist_rename -> {
|
R.id.menu_playlist_rename -> {
|
||||||
EnterPlaylistNameDialog.showForRename(activity, this, playlistName, playlistId)
|
EnterPlaylistNameDialog.showForRename(activity, this, playlistName, playlistId)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,12 @@ import io.casey.musikcube.remote.framework.ViewModel
|
|||||||
class ViewModelMixin(private val provider: ViewModel.Provider): MixinBase() {
|
class ViewModelMixin(private val provider: ViewModel.Provider): MixinBase() {
|
||||||
private var viewModel: ViewModel<*>? = null
|
private var viewModel: ViewModel<*>? = null
|
||||||
|
|
||||||
fun <T: ViewModel<*>> get(): T? = this.viewModel as T?
|
fun <T: ViewModel<*>> get(): T? {
|
||||||
|
if (viewModel == null) {
|
||||||
|
viewModel = provider.createViewModel()
|
||||||
|
}
|
||||||
|
return viewModel as T?
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(bundle: Bundle) {
|
override fun onCreate(bundle: Bundle) {
|
||||||
super.onCreate(bundle)
|
super.onCreate(bundle)
|
||||||
|
@ -40,7 +40,6 @@ class TrackListSlidingWindow(private val recyclerView: FastScrollRecyclerView,
|
|||||||
|
|
||||||
abstract class QueryFactory {
|
abstract class QueryFactory {
|
||||||
abstract fun count(): Observable<Int>?
|
abstract fun count(): Observable<Int>?
|
||||||
abstract fun all(): Observable<List<ITrack>>?
|
|
||||||
abstract fun page(offset: Int, limit: Int): Observable<List<ITrack>>?
|
abstract fun page(offset: Int, limit: Int): Observable<List<ITrack>>?
|
||||||
abstract fun offline(): Boolean
|
abstract fun offline(): Boolean
|
||||||
}
|
}
|
||||||
|
@ -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<RecyclerView>(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 <T: ViewModel<*>> 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -185,9 +185,6 @@ class TrackListActivity : BaseActivity(), Filterable {
|
|||||||
override fun count(): Observable<Int> =
|
override fun count(): Observable<Int> =
|
||||||
data.provider.getTrackCountByCategory(categoryType ?: "", categoryId, lastFilter)
|
data.provider.getTrackCountByCategory(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>> =
|
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> =
|
||||||
data.provider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter)
|
data.provider.getTracksByCategory(categoryType ?: "", categoryId, limit, offset, lastFilter)
|
||||||
|
|
||||||
@ -201,9 +198,6 @@ class TrackListActivity : BaseActivity(), Filterable {
|
|||||||
override fun count(): Observable<Int> =
|
override fun count(): Observable<Int> =
|
||||||
data.provider.getTrackCount(lastFilter)
|
data.provider.getTrackCount(lastFilter)
|
||||||
|
|
||||||
override fun all(): Observable<List<ITrack>>? =
|
|
||||||
data.provider.getTracks(lastFilter)
|
|
||||||
|
|
||||||
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> =
|
override fun page(offset: Int, limit: Int): Observable<List<ITrack>> =
|
||||||
data.provider.getTracks(limit, offset, lastFilter)
|
data.provider.getTracks(limit, offset, lastFilter)
|
||||||
|
|
||||||
|
@ -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<EditPlaylistAdapter.ViewHolder>() {
|
||||||
|
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<View>(R.id.dragHandle)
|
||||||
|
val swipe = view.findViewById<View>(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<TextView>(R.id.title)
|
||||||
|
private val subtitle = itemView.findViewById<TextView>(R.id.subtitle)
|
||||||
|
|
||||||
|
fun bind(track: ITrack) {
|
||||||
|
title.text = fallback(track.title, "-")
|
||||||
|
subtitle.text = fallback(track.albumArtist, "-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<EditPlaylistViewModel.Status>() {
|
||||||
|
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<String> = mutableListOf()
|
||||||
|
|
||||||
|
private val cache = object : LinkedHashMap<String, CacheEntry>() {
|
||||||
|
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, CacheEntry>): 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<Long> {
|
||||||
|
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<String>()
|
||||||
|
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<String, ITrack> -> list.asIterable() }
|
||||||
|
.subscribeBy(
|
||||||
|
onNext = { entry: Map.Entry<String, ITrack> ->
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<vector android:height="16dp" android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0" android:width="16dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M3,15h18v-2L3,13v2zM3,19h18v-2L3,17v2zM3,11h18L21,9L3,9v2zM3,5v2h18L21,5L3,5z"/>
|
||||||
|
</vector>
|
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/colorControlNormal"
|
||||||
|
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
|
||||||
|
</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="@color/theme_background"/>
|
||||||
|
</ripple>
|
@ -0,0 +1,85 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/rowView"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/opaque_row_background"
|
||||||
|
android:minHeight="52dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/dragHandle"
|
||||||
|
android:src="@drawable/ic_reorder"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="36dp"
|
||||||
|
android:gravity="center"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_toEndOf="@+id/dragHandle"
|
||||||
|
android:layout_toStartOf="@+id/swipeHandle"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textColor="@color/theme_foreground"
|
||||||
|
tools:text="title"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:textSize="12dp"
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textColor="@color/theme_disabled_foreground"
|
||||||
|
tools:text="subtitle"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/swipeHandle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:minHeight="52dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_alignParentEnd="true">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginEnd="-4dp"
|
||||||
|
android:scaleType="center"
|
||||||
|
android:src="@drawable/ic_leftarrow" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:background="@drawable/ic_trashcan"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="center"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
11
src/musikdroid/app/src/main/res/menu/edit_playlist_menu.xml
Normal file
11
src/musikdroid/app/src/main/res/menu/edit_playlist_menu.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_save"
|
||||||
|
app:showAsAction="always"
|
||||||
|
android:title="@string/button_save"/>
|
||||||
|
|
||||||
|
</menu>
|
@ -4,6 +4,10 @@
|
|||||||
android:id="@+id/menu_playlist_play"
|
android:id="@+id/menu_playlist_play"
|
||||||
android:title="@string/menu_play_playlist"/>
|
android:title="@string/menu_play_playlist"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_playlist_edit"
|
||||||
|
android:title="@string/menu_edit_playlist"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_playlist_rename"
|
android:id="@+id/menu_playlist_rename"
|
||||||
android:title="@string/menu_rename_playlist"/>
|
android:title="@string/menu_rename_playlist"/>
|
||||||
|
@ -78,6 +78,7 @@
|
|||||||
<string name="menu_show_genres">genres</string>
|
<string name="menu_show_genres">genres</string>
|
||||||
<string name="menu_delete_playlist">delete</string>
|
<string name="menu_delete_playlist">delete</string>
|
||||||
<string name="menu_rename_playlist">rename</string>
|
<string name="menu_rename_playlist">rename</string>
|
||||||
|
<string name="menu_edit_playlist">edit</string>
|
||||||
<string name="menu_play_playlist">play now</string>
|
<string name="menu_play_playlist">play now</string>
|
||||||
<string name="unknown_value"><unknown></string>
|
<string name="unknown_value"><unknown></string>
|
||||||
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
|
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user