Use RecycleView inside PlayerFragment

This commit is contained in:
tzugen 2021-11-15 20:01:04 +01:00
parent 6277ee73c0
commit d243ae1b44
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
37 changed files with 393 additions and 343 deletions

View File

@ -30,7 +30,6 @@ ext.versions = [
okhttp : "3.12.13", okhttp : "3.12.13",
koin : "3.0.2", koin : "3.0.2",
picasso : "2.71828", picasso : "2.71828",
sortListView : "1.0.1",
junit4 : "4.13.2", junit4 : "4.13.2",
junit5 : "5.8.1", junit5 : "5.8.1",
@ -92,7 +91,6 @@ ext.other = [
dexter : "com.karumi:dexter:$versions.dexter", dexter : "com.karumi:dexter:$versions.dexter",
timber : "com.jakewharton.timber:timber:$versions.timber", timber : "com.jakewharton.timber:timber:$versions.timber",
fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll", fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll",
sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView",
colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker", colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker",
rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava", rxJava : "io.reactivex.rxjava3:rxjava:$versions.rxJava",
rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid", rxAndroid : "io.reactivex.rxjava3:rxandroid:$versions.rxAndroid",

View File

@ -104,7 +104,6 @@ dependencies {
implementation other.koinAndroid implementation other.koinAndroid
implementation other.okhttpLogging implementation other.okhttpLogging
implementation other.fastScroll implementation other.fastScroll
implementation other.sortListView
implementation other.colorPickerView implementation other.colorPickerView
implementation other.rxJava implementation other.rxJava
implementation other.rxAndroid implementation other.rxAndroid

View File

@ -1,16 +1,16 @@
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import java.util.HashSet
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName import org.moire.ultrasonic.util.Settings.shouldUseFolderForArtistName
import org.moire.ultrasonic.util.Util.getGrandparent import org.moire.ultrasonic.util.Util.getGrandparent
import java.util.HashSet
class AlbumHeader( class AlbumHeader(
var entries: List<MusicDirectory.Entry>, var entries: List<MusicDirectory.Entry>,
var name: String, var name: String,
songCount: Int songCount: Int
): Identifiable { ) : Identifiable {
var isAllVideo: Boolean var isAllVideo: Boolean
private set private set
@ -72,7 +72,6 @@ class AlbumHeader(
} }
} }
init { init {
_artists = HashSet() _artists = HashSet()
_grandParents = HashSet() _grandParents = HashSet()

View File

@ -37,6 +37,8 @@ import org.moire.ultrasonic.util.Util;
* *
* @author Sindre Mehus * @author Sindre Mehus
*/ */
public class AlbumView extends UpdateView public class AlbumView extends UpdateView
{ {
private static Drawable starDrawable; private static Drawable starDrawable;

View File

@ -1,55 +0,0 @@
package org.moire.ultrasonic.view;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import org.moire.ultrasonic.domain.MusicDirectory;
import org.moire.ultrasonic.service.DownloadFile;
import java.util.List;
public class SongListAdapter extends ArrayAdapter<DownloadFile>
{
Context context;
public SongListAdapter(Context context, final List<DownloadFile> entries)
{
super(context, android.R.layout.simple_list_item_1, entries);
this.context = context;
}
@Override
public View getView(final int position, final View convertView, final ViewGroup parent)
{
DownloadFile downloadFile = getItem(position);
MusicDirectory.Entry entry = downloadFile.getSong();
SongView view;
if (convertView instanceof SongView)
{
SongView currentView = (SongView) convertView;
if (currentView.getEntry().equals(entry))
{
currentView.update();
return currentView;
}
else
{
EntryAdapter.SongViewHolder viewHolder = (EntryAdapter.SongViewHolder) convertView.getTag();
view = currentView;
view.setViewHolder(viewHolder);
}
}
else
{
view = new SongView(this.context);
view.setLayout(entry);
}
view.setSong(entry, false, true);
return view;
}
}

View File

@ -8,15 +8,14 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.drakeet.multitype.ItemViewBinder import com.drakeet.multitype.ItemViewBinder
import java.lang.ref.WeakReference
import java.util.Random
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.subsonic.ImageLoaderProvider import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.AlbumHeader import org.moire.ultrasonic.util.AlbumHeader
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import java.lang.ref.WeakReference
import java.util.Random
/** /**
* This Binder can bind a list of entries into a Header * This Binder can bind a list of entries into a Header
@ -51,7 +50,6 @@ class HeaderViewBinder(
val context = weakContext.get() ?: return val context = weakContext.get() ?: return
val resources = context.resources val resources = context.resources
val artworkSelection = random.nextInt(item.childCount) val artworkSelection = random.nextInt(item.childCount)
imageLoaderProvider.getImageLoader().loadImage( imageLoaderProvider.getImageLoader().loadImage(
@ -61,7 +59,6 @@ class HeaderViewBinder(
holder.titleView.text = item.name holder.titleView.text = item.name
// Don't show a header if all entries are videos // Don't show a header if all entries are videos
if (item.isAllVideo) { if (item.isAllVideo) {
return return
@ -74,7 +71,6 @@ class HeaderViewBinder(
} }
holder.artistView.text = artist holder.artistView.text = artist
val genre: String = if (item.genres.size == 1) { val genre: String = if (item.genres.size == 1) {
item.genres.iterator().next() item.genres.iterator().next()
} else { } else {
@ -83,7 +79,6 @@ class HeaderViewBinder(
holder.genreView.text = genre holder.genreView.text = genre
val year: String = if (item.years.size == 1) { val year: String = if (item.years.size == 1) {
item.years.iterator().next().toString() item.years.iterator().next().toString()
} else { } else {
@ -92,7 +87,6 @@ class HeaderViewBinder(
holder.yearView.text = year holder.yearView.text = year
val songs = resources.getQuantityString( val songs = resources.getQuantityString(
R.plurals.select_album_n_songs, item.childCount, R.plurals.select_album_n_songs, item.childCount,
item.childCount item.childCount

View File

@ -25,7 +25,7 @@ class ImageHelper(context: Context) {
val themesMatch = theme == currentTheme val themesMatch = theme == currentTheme
if (!themesMatch) theme = currentTheme if (!themesMatch) theme = currentTheme
if (!themesMatch || force ) { if (!themesMatch || force) {
getDrawables(context) getDrawables(context)
} }
} }

View File

@ -8,8 +8,8 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import com.drakeet.multitype.MultiTypeAdapter import com.drakeet.multitype.MultiTypeAdapter
import org.moire.ultrasonic.domain.Identifiable
import java.util.TreeSet import java.util.TreeSet
import org.moire.ultrasonic.domain.Identifiable
class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() { class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
@ -36,7 +36,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
throw IllegalAccessException("You must use submitList() to add data to the MultiTypeDiffAdapter") throw IllegalAccessException("You must use submitList() to add data to the MultiTypeDiffAdapter")
} }
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer( var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
AdapterListUpdateCallback(this), AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build() AsyncDifferConfig.Builder(diffCallback).build()
@ -54,7 +53,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
mDiffer.addListListener(mListener) mDiffer.addListListener(mListener)
} }
/** /**
* Submits a new list to be diffed, and displayed. * Submits a new list to be diffed, and displayed.
* *
@ -88,8 +86,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
mDiffer.submitList(list, commitCallback) mDiffer.submitList(list, commitCallback)
} }
override fun getItemCount(): Int { override fun getItemCount(): Int {
return mDiffer.currentList.size return mDiffer.currentList.size
} }
@ -151,7 +147,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
selectionRevision.postValue(selectionRevision.value!! + 1) selectionRevision.postValue(selectionRevision.value!! + 1)
} }
fun setSelectionStatusOfAll(select: Boolean): Int { fun setSelectionStatusOfAll(select: Boolean): Int {
// Clear current selection // Clear current selection
selectedSet.clear() selectedSet.clear()
@ -163,10 +158,13 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
if (!select) return 0 if (!select) return 0
// Select them all // Select them all
getCurrentList().mapNotNullTo(selectedSet, { entry -> getCurrentList().mapNotNullTo(
// Exclude any -1 ids, eg. headers and other UI elements selectedSet,
entry.longId.takeIf { it != -1L } { entry ->
}) // Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
}
)
return selectedSet.count() return selectedSet.count()
} }
@ -175,6 +173,18 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
return selectedSet.contains(longId) return selectedSet.contains(longId)
} }
fun moveItem(from: Int, to: Int): List<T> {
val list = getCurrentList().toMutableList()
val fromLocation = list[from]
list.removeAt(from)
if (to < from) {
list.add(to + 1, fromLocation)
} else {
list.add(to - 1, fromLocation)
}
submitList(list)
return list as List<T>
}
companion object { companion object {
/** /**
@ -190,8 +200,5 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }
} }
} }
} }

View File

@ -18,7 +18,6 @@ import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
/** /**

View File

@ -2,6 +2,7 @@ package org.moire.ultrasonic.adapters
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.drakeet.multitype.ItemViewBinder import com.drakeet.multitype.ItemViewBinder
@ -12,16 +13,15 @@ import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.service.Downloader
import timber.log.Timber
class TrackViewBinder( class TrackViewBinder(
val checkable: Boolean, val checkable: Boolean,
val draggable: Boolean, val draggable: Boolean,
context: Context, context: Context,
val lifecycleOwner: LifecycleOwner val lifecycleOwner: LifecycleOwner,
private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null
) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent { ) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent {
// // // //
// onItemClick: (MusicDirectory.Entry) -> Unit, // onItemClick: (MusicDirectory.Entry) -> Unit,
// onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean, // onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
@ -40,12 +40,13 @@ class TrackViewBinder(
private val imageHelper: ImageHelper = ImageHelper(context) private val imageHelper: ImageHelper = ImageHelper(context)
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): TrackViewHolder { override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): TrackViewHolder {
return TrackViewHolder(inflater.inflate(layout, parent, false), adapter as MultiTypeDiffAdapter<Identifiable>) return TrackViewHolder(inflater.inflate(layout, parent, false))
} }
override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) { override fun onBindViewHolder(holder: TrackViewHolder, item: Identifiable) {
val downloadFile: DownloadFile? val downloadFile: DownloadFile?
val _adapter = adapter as MultiTypeDiffAdapter<*>
when (item) { when (item) {
is MusicDirectory.Entry -> { is MusicDirectory.Entry -> {
@ -65,33 +66,47 @@ class TrackViewBinder(
file = downloadFile, file = downloadFile,
checkable = checkable, checkable = checkable,
draggable = draggable, draggable = draggable,
holder.adapter.isSelected(item.longId) _adapter.isSelected(item.longId)
)
// Notify the adapter of selection changes
holder.observableChecked.observe(
lifecycleOwner,
{ newValue ->
if (newValue) {
_adapter.notifySelected(item.longId)
} else {
_adapter.notifyUnselected(item.longId)
}
}
) )
// Listen to changes in selection status and update ourselves // Listen to changes in selection status and update ourselves
holder.adapter.selectionRevision.observe(lifecycleOwner, { _adapter.selectionRevision.observe(
val newStatus = holder.adapter.isSelected(item.longId) lifecycleOwner,
{
val newStatus = _adapter.isSelected(item.longId)
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
})
// Observe download status
downloadFile.status.observe(lifecycleOwner, {
Timber.w("CAUGHT STATUS CHANGE")
holder.updateStatus(it)
holder.adapter.notifyChanged()
} }
) )
downloadFile.progress.observe(lifecycleOwner, { // Observe download status
Timber.w("CAUGHT PROGRESS CHANGE") downloadFile.status.observe(
lifecycleOwner,
{
holder.updateStatus(it)
_adapter.notifyChanged()
}
)
downloadFile.progress.observe(
lifecycleOwner,
{
holder.updateProgress(it) holder.updateProgress(it)
} }
) )
holder.itemClickListener = onClickCallback
} }
} }

View File

@ -9,13 +9,13 @@ import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.featureflags.Feature import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage import org.moire.ultrasonic.featureflags.FeatureStorage
@ -31,8 +31,7 @@ import timber.log.Timber
* Used to display songs and videos in a `ListView`. * Used to display songs and videos in a `ListView`.
* TODO: Video List item * TODO: Video List item
*/ */
class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifiable>) : class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
RecyclerView.ViewHolder(view), Checkable, KoinComponent {
var check: CheckedTextView = view.findViewById(R.id.song_check) var check: CheckedTextView = view.findViewById(R.id.song_check)
var rating: LinearLayout = view.findViewById(R.id.song_rating) var rating: LinearLayout = view.findViewById(R.id.song_rating)
@ -49,6 +48,8 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
var duration: TextView = view.findViewById(R.id.song_duration) var duration: TextView = view.findViewById(R.id.song_duration)
var progress: TextView = view.findViewById(R.id.song_status) var progress: TextView = view.findViewById(R.id.song_status)
var itemClickListener: ((View, DownloadFile?) -> Unit)? = null
var entry: MusicDirectory.Entry? = null var entry: MusicDirectory.Entry? = null
private set private set
var downloadFile: DownloadFile? = null var downloadFile: DownloadFile? = null
@ -59,6 +60,8 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
private var statusImage: Drawable? = null private var statusImage: Drawable? = null
private var playing = false private var playing = false
var observableChecked = MutableLiveData(false)
private val useFiveStarRating: Boolean by lazy { private val useFiveStarRating: Boolean by lazy {
val features: FeatureStorage = get() val features: FeatureStorage = get()
features.isFeatureEnabled(Feature.FIVE_STAR_RATING) features.isFeatureEnabled(Feature.FIVE_STAR_RATING)
@ -67,11 +70,15 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
private val mediaPlayerController: MediaPlayerController by inject() private val mediaPlayerController: MediaPlayerController by inject()
lateinit var imageHelper: ImageHelper lateinit var imageHelper: ImageHelper
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
val nowChecked = !check.isChecked if (itemClickListener != null) {
isChecked = nowChecked itemClickListener?.invoke(it, downloadFile)
} else {
val nowChecked = !check.isChecked
isChecked = nowChecked
}
} }
} }
@ -92,7 +99,6 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
title.text = entryDescription.title title.text = entryDescription.title
duration.text = entryDescription.duration duration.text = entryDescription.duration
if (Settings.shouldShowTrackNumber && song.track != null && song.track!! > 0) { if (Settings.shouldShowTrackNumber && song.track != null && song.track!! > 0) {
track.text = entryDescription.trackNumber track.text = entryDescription.trackNumber
} else { } else {
@ -100,7 +106,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
} }
check.isVisible = (checkable && !song.isVideo) check.isVisible = (checkable && !song.isVideo)
check.isChecked = isSelected setCheckedSilent(isSelected)
drag.isVisible = draggable drag.isVisible = draggable
if (ActiveServerProvider.isOffline()) { if (ActiveServerProvider.isOffline()) {
@ -109,7 +115,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
} else { } else {
setupStarButtons(song) setupStarButtons(song)
} }
update() update()
} }
@ -151,9 +157,6 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
} }
} }
@Synchronized @Synchronized
// TODO: Should be removed // TODO: Should be removed
fun update() { fun update() {
@ -218,12 +221,10 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
} }
} }
fun updateStatus(status: DownloadStatus) { fun updateStatus(status: DownloadStatus) {
if (status == cachedStatus) return if (status == cachedStatus) return
cachedStatus = status cachedStatus = status
Timber.w("STATUS: %s", status) Timber.w("STATUS: %s", status)
when (status) { when (status) {
@ -254,7 +255,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
fun updateProgress(p: Int) { fun updateProgress(p: Int) {
if (cachedStatus == DownloadStatus.DOWNLOADING) { if (cachedStatus == DownloadStatus.DOWNLOADING) {
progress.text = Util.formatPercentage(p) progress.text = Util.formatPercentage(p)
} else { } else {
progress.text = null progress.text = null
} }
} }
@ -271,13 +272,12 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
} }
} }
private fun setCheckedSilent(newStatus: Boolean) {
check.isChecked = newStatus
}
override fun setChecked(newStatus: Boolean) { override fun setChecked(newStatus: Boolean) {
if (newStatus) { observableChecked.postValue(newStatus)
adapter.notifySelected(downloadFile!!.longId)
} else {
adapter.notifyUnselected(downloadFile!!.longId)
}
check.isChecked = newStatus check.isChecked = newStatus
} }

View File

@ -0,0 +1,69 @@
package org.moire.ultrasonic.adapters.legacy
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.lifecycle.LifecycleOwner
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.ImageHelper
import org.moire.ultrasonic.adapters.TrackViewHolder
import org.moire.ultrasonic.service.DownloadFile
/**
* Legacy bridge to provide Views to a ListView using RecyclerView.ViewHolders
*/
class SongListAdapter(
ctx: Context,
entries: List<DownloadFile?>?,
val lifecycleOwner: LifecycleOwner
) :
ArrayAdapter<DownloadFile?>(ctx, android.R.layout.simple_list_item_1, entries!!) {
val layout = R.layout.song_list_item
private val imageHelper: ImageHelper = ImageHelper(context)
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val downloadFile = getItem(position)!!
var view = convertView
val holder: TrackViewHolder
if (view == null) {
val inflater = LayoutInflater.from(context)
view = inflater.inflate(layout, parent, false)
}
if (view?.tag is TrackViewHolder) {
holder = view.tag as TrackViewHolder
} else {
holder = TrackViewHolder(view!!)
view.tag = holder
}
holder.imageHelper = imageHelper
holder.setSong(
file = downloadFile,
checkable = false,
draggable = true
)
// Observe download status
downloadFile.status.observe(
lifecycleOwner,
{
holder.updateStatus(it)
}
)
downloadFile.progress.observe(
lifecycleOwner,
{
holder.updateProgress(it)
}
)
return view
}
}

View File

@ -8,9 +8,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.DownloadFile import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
@ -54,7 +52,7 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
viewAdapter.register( viewAdapter.register(
TrackViewBinder( TrackViewBinder(
checkable = true, checkable = false,
draggable = false, draggable = false,
context = requireContext(), context = requireContext(),
lifecycleOwner = viewLifecycleOwner lifecycleOwner = viewLifecycleOwner
@ -65,7 +63,6 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
} }
} }
class DownloadListModel(application: Application) : GenericListModel(application) { class DownloadListModel(application: Application) : GenericListModel(application) {
private val downloader by inject<Downloader>() private val downloader by inject<Downloader>()
@ -73,6 +70,3 @@ class DownloadListModel(application: Application) : GenericListModel(application
return downloader.observableDownloads return downloader.observableDownloads
} }
} }

View File

@ -8,18 +8,14 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.drakeet.multitype.MultiTypeAdapter
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Artist
import org.moire.ultrasonic.domain.GenericEntry
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicFolder import org.moire.ultrasonic.domain.MusicFolder
import org.moire.ultrasonic.subsonic.DownloadHandler import org.moire.ultrasonic.subsonic.DownloadHandler
@ -32,7 +28,6 @@ import org.moire.ultrasonic.view.SelectMusicFolderView
/** /**
* An abstract Model, which can be extended to display a list of items of type T from the API * An abstract Model, which can be extended to display a list of items of type T from the API
* @param T: The type of data which will be used (must extend GenericEntry) * @param T: The type of data which will be used (must extend GenericEntry)
* @param TA: The Adapter to use (must extend GenericRowAdapter)
*/ */
abstract class MultiListFragment<T : Identifiable> : Fragment() { abstract class MultiListFragment<T : Identifiable> : Fragment() {
internal val activeServerProvider: ActiveServerProvider by inject() internal val activeServerProvider: ActiveServerProvider by inject()
@ -94,7 +89,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
*/ */
@Suppress("CommentOverPrivateProperty") @Suppress("CommentOverPrivateProperty")
private val musicFolderObserver = { folders: List<MusicFolder> -> private val musicFolderObserver = { folders: List<MusicFolder> ->
//viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId) // viewAdapter.setFolderList(folders, listModel.activeServer.musicFolderId)
} }
/** /**
@ -115,7 +110,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
*/ */
fun showFolderHeader(): Boolean { fun showFolderHeader(): Boolean {
return listModel.showSelectFolderHeader(arguments) && return listModel.showSelectFolderHeader(arguments) &&
!listModel.isOffline() && !Settings.shouldUseId3Tags !listModel.isOffline() && !Settings.shouldUseId3Tags
} }
open fun setTitle(title: String?) { open fun setTitle(title: String?) {
@ -147,9 +142,13 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
liveDataItems = getLiveData(arguments) liveDataItems = getLiveData(arguments)
// Register an observer to update our UI when the data changes // Register an observer to update our UI when the data changes
liveDataItems.observe(viewLifecycleOwner, { liveDataItems.observe(
newItems -> viewAdapter.submitList(newItems) viewLifecycleOwner,
}) {
newItems ->
viewAdapter.submitList(newItems)
}
)
// Setup the Music folder handling // Setup the Music folder handling
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver) listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
@ -165,7 +164,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
} }
// Configure whether to show the folder header // Configure whether to show the folder header
//viewAdapter.folderHeaderEnabled = showFolderHeader() // viewAdapter.folderHeaderEnabled = showFolderHeader()
} }
@Override @Override
@ -187,7 +186,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
abstract fun onItemClick(item: T) abstract fun onItemClick(item: T)
} }
//abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> : // abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> :
// GenericListFragment<T, TA>() { // GenericListFragment<T, TA>() {
// @Suppress("LongMethod") // @Suppress("LongMethod")
// override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean { // override fun onContextMenuItemSelected(menuItem: MenuItem, item: T): Boolean {
@ -284,4 +283,4 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
// bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist)) // bundle.putBoolean(Constants.INTENT_EXTRA_NAME_ARTIST, (item is Artist))
// findNavController().navigate(itemClickTarget, bundle) // findNavController().navigate(itemClickTarget, bundle)
// } // }
//} // }

View File

@ -36,8 +36,10 @@ import android.widget.ViewFlipper
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.Navigation import androidx.navigation.Navigation
import com.mobeta.android.dslv.DragSortListView import androidx.recyclerview.widget.ItemTouchHelper
import com.mobeta.android.dslv.DragSortListView.DragSortListener import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -58,9 +60,12 @@ import org.koin.android.ext.android.inject
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.audiofx.EqualizerController import org.moire.ultrasonic.audiofx.EqualizerController
import org.moire.ultrasonic.audiofx.VisualizerController import org.moire.ultrasonic.audiofx.VisualizerController
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.domain.PlayerState
import org.moire.ultrasonic.domain.RepeatMode import org.moire.ultrasonic.domain.RepeatMode
@ -81,7 +86,6 @@ import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.view.AutoRepeatButton import org.moire.ultrasonic.view.AutoRepeatButton
import org.moire.ultrasonic.view.SongListAdapter
import org.moire.ultrasonic.view.VisualizerView import org.moire.ultrasonic.view.VisualizerView
import timber.log.Timber import timber.log.Timber
@ -94,6 +98,8 @@ class PlayerFragment :
GestureDetector.OnGestureListener, GestureDetector.OnGestureListener,
KoinComponent, KoinComponent,
CoroutineScope by CoroutineScope(Dispatchers.Main) { CoroutineScope by CoroutineScope(Dispatchers.Main) {
// Settings
private var swipeDistance = 0 private var swipeDistance = 0
private var swipeVelocity = 0 private var swipeVelocity = 0
private var jukeboxAvailable = false private var jukeboxAvailable = false
@ -104,6 +110,7 @@ class PlayerFragment :
// Detectors & Callbacks // Detectors & Callbacks
private lateinit var gestureScanner: GestureDetector private lateinit var gestureScanner: GestureDetector
private lateinit var cancellationToken: CancellationToken private lateinit var cancellationToken: CancellationToken
private lateinit var dragTouchHelper: ItemTouchHelper
// Data & Services // Data & Services
private val networkAndStorageChecker: NetworkAndStorageChecker by inject() private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
@ -114,6 +121,7 @@ class PlayerFragment :
private lateinit var executorService: ScheduledExecutorService private lateinit var executorService: ScheduledExecutorService
private var currentPlaying: DownloadFile? = null private var currentPlaying: DownloadFile? = null
private var currentSong: MusicDirectory.Entry? = null private var currentSong: MusicDirectory.Entry? = null
private lateinit var viewManager: LinearLayoutManager
private var rxBusSubscription: Disposable? = null private var rxBusSubscription: Disposable? = null
private var ioScope = CoroutineScope(Dispatchers.IO) private var ioScope = CoroutineScope(Dispatchers.IO)
@ -133,7 +141,7 @@ class PlayerFragment :
private lateinit var albumTextView: TextView private lateinit var albumTextView: TextView
private lateinit var artistTextView: TextView private lateinit var artistTextView: TextView
private lateinit var albumArtImageView: ImageView private lateinit var albumArtImageView: ImageView
private lateinit var playlistView: DragSortListView private lateinit var playlistView: RecyclerView
private lateinit var positionTextView: TextView private lateinit var positionTextView: TextView
private lateinit var downloadTrackTextView: TextView private lateinit var downloadTrackTextView: TextView
private lateinit var downloadTotalDurationTextView: TextView private lateinit var downloadTotalDurationTextView: TextView
@ -146,6 +154,10 @@ class PlayerFragment :
private lateinit var fullStar: Drawable private lateinit var fullStar: Drawable
private lateinit var progressBar: SeekBar private lateinit var progressBar: SeekBar
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
MultiTypeDiffAdapter()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context) Util.applyTheme(this.context)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -322,14 +334,7 @@ class PlayerFragment :
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
}) })
playlistView.setOnItemClickListener { _, _, position, _ -> initPlaylistDisplay()
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
launch(CommunicationError.getHandler(context)) {
mediaPlayerController.play(position)
onCurrentChanged()
onSliderProgressChanged()
}
}
registerForContextMenu(playlistView) registerForContextMenu(playlistView)
@ -432,15 +437,12 @@ class PlayerFragment :
// Scroll to current playing. // Scroll to current playing.
private fun scrollToCurrent() { private fun scrollToCurrent() {
val adapter = playlistView.adapter val index = mediaPlayerController.playList.indexOf(currentPlaying)
if (adapter != null) {
val count = adapter.count if (index != -1) {
for (i in 0 until count) { val smoothScroller = LinearSmoothScroller(context)
if (currentPlaying == playlistView.getItemAtPosition(i)) { smoothScroller.targetPosition = index
playlistView.smoothScrollToPositionFromTop(i, 40) viewManager.startSmoothScroll(smoothScroller)
return
}
}
} }
} }
@ -535,7 +537,7 @@ class PlayerFragment :
super.onCreateContextMenu(menu, view, menuInfo) super.onCreateContextMenu(menu, view, menuInfo)
if (view === playlistView) { if (view === playlistView) {
val info = menuInfo as AdapterContextMenuInfo? val info = menuInfo as AdapterContextMenuInfo?
val downloadFile = playlistView.getItemAtPosition(info!!.position) as DownloadFile val downloadFile = viewAdapter.getCurrentList()[info!!.position] as DownloadFile
val menuInflater = requireActivity().menuInflater val menuInflater = requireActivity().menuInflater
menuInflater.inflate(R.menu.nowplaying_context, menu) menuInflater.inflate(R.menu.nowplaying_context, menu)
val song: MusicDirectory.Entry? val song: MusicDirectory.Entry?
@ -561,7 +563,7 @@ class PlayerFragment :
override fun onContextItemSelected(menuItem: MenuItem): Boolean { override fun onContextItemSelected(menuItem: MenuItem): Boolean {
val info = menuItem.menuInfo as AdapterContextMenuInfo val info = menuItem.menuInfo as AdapterContextMenuInfo
val downloadFile = playlistView.getItemAtPosition(info.position) as DownloadFile val downloadFile = viewAdapter.getCurrentList()[info.position] as DownloadFile
return menuItemSelected(menuItem.itemId, downloadFile) || super.onContextItemSelected( return menuItemSelected(menuItem.itemId, downloadFile) || super.onContextItemSelected(
menuItem menuItem
) )
@ -842,43 +844,71 @@ class PlayerFragment :
} }
} }
private fun initPlaylistDisplay() {
// Create a View Manager
viewManager = LinearLayoutManager(this.context)
// Hook up the view with the manager and the adapter
playlistView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
// Create listener
val listener: ((View, DownloadFile?) -> Unit) = { _, file ->
val list = mediaPlayerController.playList
val index = list.indexOf(file)
mediaPlayerController.play(index)
onCurrentChanged()
onSliderProgressChanged()
}
viewAdapter.register(
TrackViewBinder(
checkable = false,
draggable = true,
context = requireContext(),
lifecycleOwner = viewLifecycleOwner,
listener
)
)
dragTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
// FIXME:
// Needs to be changed in the playlist as well...
// Move it in the data set
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}
)
dragTouchHelper.attachToRecyclerView(playlistView)
}
private fun onPlaylistChanged() { private fun onPlaylistChanged() {
val mediaPlayerController = mediaPlayerController val mediaPlayerController = mediaPlayerController
val list = mediaPlayerController.playList val list = mediaPlayerController.playList
emptyTextView.setText(R.string.download_empty) emptyTextView.setText(R.string.playlist_empty)
val adapter = SongListAdapter(context, list)
playlistView.adapter = adapter
playlistView.setDragSortListener(object : DragSortListener {
override fun drop(from: Int, to: Int) {
if (from != to) {
val item = adapter.getItem(from)
adapter.remove(item)
adapter.notifyDataSetChanged()
adapter.insert(item, to)
adapter.notifyDataSetChanged()
}
}
override fun drag(from: Int, to: Int) {} viewAdapter.submitList(list)
override fun remove(which: Int) {
val item = adapter.getItem(which) ?: return
val currentPlaying = mediaPlayerController.currentPlaying
if (currentPlaying == item) {
mediaPlayerController.next()
}
adapter.remove(item)
adapter.notifyDataSetChanged()
val songRemoved = String.format(
resources.getString(R.string.download_song_removed),
item.song.title
)
Util.toast(context, songRemoved)
onPlaylistChanged()
onCurrentChanged()
}
})
emptyTextView.isVisible = list.isEmpty() emptyTextView.isVisible = list.isEmpty()

View File

@ -10,12 +10,10 @@ package org.moire.ultrasonic.fragment
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView.AdapterContextMenuInfo import android.widget.AdapterView.AdapterContextMenuInfo
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
@ -27,12 +25,12 @@ import androidx.lifecycle.viewModelScope
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.Collections
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.HeaderViewBinder import org.moire.ultrasonic.adapters.HeaderViewBinder
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
import org.moire.ultrasonic.domain.Identifiable import org.moire.ultrasonic.domain.Identifiable
@ -49,7 +47,6 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import timber.log.Timber import timber.log.Timber
import java.util.Collections
/** /**
* Displays a group of tracks, eg. the songs of an album, of a playlist etc. * Displays a group of tracks, eg. the songs of an album, of a playlist etc.
@ -106,7 +103,6 @@ class TrackCollectionFragment :
// FIXME // FIXME
override val itemClickTarget: Int = R.id.trackCollectionFragment override val itemClickTarget: Int = R.id.trackCollectionFragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
cancellationToken = CancellationToken() cancellationToken = CancellationToken()
@ -232,9 +228,12 @@ class TrackCollectionFragment :
enableButtons() enableButtons()
// Update the buttons when the selection has changed // Update the buttons when the selection has changed
viewAdapter.selectionRevision.observe(viewLifecycleOwner, { viewAdapter.selectionRevision.observe(
enableButtons() viewLifecycleOwner,
}) {
enableButtons()
}
)
// Loads the data // Loads the data
updateDisplay(false) updateDisplay(false)
@ -454,7 +453,6 @@ class TrackCollectionFragment :
val toastResId = R.string.select_album_n_selected val toastResId = R.string.select_album_n_selected
Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0))) Util.toast(activity, getString(toastResId, selectedCount.coerceAtLeast(0)))
} }
} }
private fun enableButtons(selection: List<MusicDirectory.Entry> = getSelectedSongs()) { private fun enableButtons(selection: List<MusicDirectory.Entry> = getSelectedSongs()) {
@ -519,12 +517,14 @@ class TrackCollectionFragment :
} }
private fun delete() { private fun delete() {
var songs = getSelectedSongs() val songs = getSelectedSongs()
if (songs.isEmpty()) { Util.toast(
selectAll(selected = true, toast = false) context,
songs = getSelectedSongs() resources.getQuantityString(
} R.plurals.select_album_n_songs_deleted, songs.size, songs.size
)
)
mediaPlayerController.delete(songs) mediaPlayerController.delete(songs)
} }
@ -544,8 +544,8 @@ class TrackCollectionFragment :
// Hide more button when results are less than album list size // Hide more button when results are less than album list size
if (musicDirectory.getChildren().size < requireArguments().getInt( if (musicDirectory.getChildren().size < requireArguments().getInt(
Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0 Constants.INTENT_EXTRA_NAME_ALBUM_LIST_SIZE, 0
) )
) { ) {
moreButton!!.visibility = View.GONE moreButton!!.visibility = View.GONE
} else { } else {
@ -568,7 +568,6 @@ class TrackCollectionFragment :
} }
} }
private val updateInterfaceWithEntries = Observer<List<MusicDirectory.Entry>> { private val updateInterfaceWithEntries = Observer<List<MusicDirectory.Entry>> {
val entryList: MutableList<MusicDirectory.Entry> = it.toMutableList() val entryList: MutableList<MusicDirectory.Entry> = it.toMutableList()
@ -577,7 +576,6 @@ class TrackCollectionFragment :
Collections.sort(entryList, EntryByDiscAndTrackComparator()) Collections.sort(entryList, EntryByDiscAndTrackComparator())
} }
var allVideos = true var allVideos = true
var songCount = 0 var songCount = 0
@ -650,14 +648,6 @@ class TrackCollectionFragment :
playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos playAllButtonVisible = !(isAlbumList || entryList.isEmpty()) && !allVideos
shareButtonVisible = !isOffline() && songCount > 0 shareButtonVisible = !isOffline() && songCount > 0
// TODO!!
// listView!!.removeHeaderView(emptyView!!)
// if (entries.isEmpty()) {
// emptyView!!.text = getString(R.string.select_album_empty)
// emptyView!!.setPadding(10, 10, 10, 10)
// listView!!.addHeaderView(emptyView, null, false)
// }
if (playAllButton != null) { if (playAllButton != null) {
playAllButton!!.isVisible = playAllButtonVisible playAllButton!!.isVisible = playAllButtonVisible
} }
@ -666,11 +656,10 @@ class TrackCollectionFragment :
shareButton!!.isVisible = shareButtonVisible shareButton!!.isVisible = shareButtonVisible
} }
if (songCount > 0 && listModel.showHeader) { if (songCount > 0 && listModel.showHeader) {
val name = listModel.currentDirectory.value?.name val name = listModel.currentDirectory.value?.name
val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!! val intentAlbumName = requireArguments().getString(Constants.INTENT_EXTRA_NAME_NAME, "Name")!!
val albumHeader = AlbumHeader(it, name?: intentAlbumName, songCount) val albumHeader = AlbumHeader(it, name ?: intentAlbumName, songCount)
val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader) val mixedList: MutableList<Identifiable> = mutableListOf(albumHeader)
mixedList.addAll(entryList) mixedList.addAll(entryList)
viewAdapter.submitList(mixedList) viewAdapter.submitList(mixedList)
@ -678,7 +667,6 @@ class TrackCollectionFragment :
viewAdapter.submitList(entryList) viewAdapter.submitList(entryList)
} }
val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false) val playAll = requireArguments().getBoolean(Constants.INTENT_EXTRA_NAME_AUTOPLAY, false)
if (playAll && songCount > 0) { if (playAll && songCount > 0) {
playAll( playAll(
@ -688,8 +676,6 @@ class TrackCollectionFragment :
} }
listModel.currentListIsSortable = true listModel.currentListIsSortable = true
} }
private fun getSelectedSongs(): List<MusicDirectory.Entry> { private fun getSelectedSongs(): List<MusicDirectory.Entry> {
@ -702,8 +688,6 @@ class TrackCollectionFragment :
} }
} }
override fun setTitle(title: String?) { override fun setTitle(title: String?) {
setTitle(this@TrackCollectionFragment, title) setTitle(this@TrackCollectionFragment, title)
} }
@ -787,16 +771,11 @@ class TrackCollectionFragment :
menuItem: MenuItem, menuItem: MenuItem,
item: MusicDirectory.Entry item: MusicDirectory.Entry
): Boolean { ): Boolean {
//TODO // TODO
return false return false
} }
override fun onItemClick(item: MusicDirectory.Entry) { override fun onItemClick(item: MusicDirectory.Entry) {
// nothing // nothing
} }
} }

View File

@ -13,11 +13,8 @@ import androidx.lifecycle.MutableLiveData
import java.util.LinkedList import java.util.LinkedList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.inject
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.domain.MusicDirectory import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.service.MusicService import org.moire.ultrasonic.service.MusicService
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings

View File

@ -89,7 +89,6 @@ class DownloadFile(
} }
status = MutableLiveData(state) status = MutableLiveData(state)
} }
/** /**

View File

@ -491,4 +491,3 @@ class Downloader(
} }
} }
} }

View File

@ -0,0 +1,31 @@
package org.moire.ultrasonic.util
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.ItemTouchHelper.DOWN
import androidx.recyclerview.widget.ItemTouchHelper.UP
import androidx.recyclerview.widget.RecyclerView
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import timber.log.Timber
class DragSortCallback : ItemTouchHelper.SimpleCallback(UP or DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val from = viewHolder.bindingAdapterPosition
val to = target.bindingAdapterPosition
Timber.w("MOVED %s %s", to, from)
// Move it in the data set
(recyclerView.adapter as MultiTypeDiffAdapter<*>).moveItem(from, to)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
}

View File

@ -862,7 +862,6 @@ object Util {
var fileFormat: String?, var fileFormat: String?,
) )
fun getMediaDescriptionForEntry( fun getMediaDescriptionForEntry(
song: MusicDirectory.Entry, song: MusicDirectory.Entry,
mediaId: String? = null, mediaId: String? = null,
@ -921,8 +920,8 @@ object Util {
if (artistName != null) { if (artistName != null) {
if (Settings.shouldDisplayBitrateWithArtist && ( if (Settings.shouldDisplayBitrateWithArtist && (
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
) )
) { ) {
artist.append(artistName).append(" (").append( artist.append(artistName).append(" (").append(
String.format( String.format(
@ -939,7 +938,6 @@ object Util {
val trackNumber = song.track ?: 0 val trackNumber = song.track ?: 0
val title = StringBuilder(LINE_LENGTH) val title = StringBuilder(LINE_LENGTH)
if (Settings.shouldShowTrackNumber && trackNumber > 0) { if (Settings.shouldShowTrackNumber && trackNumber > 0) {
trackText = String.format(Locale.ROOT, "%02d.", trackNumber) trackText = String.format(Locale.ROOT, "%02d.", trackNumber)

View File

@ -1,37 +1,37 @@
//package org.moire.ultrasonic.view // package org.moire.ultrasonic.view
// //
//import android.content.Context // import android.content.Context
//import android.graphics.drawable.AnimationDrawable // import android.graphics.drawable.AnimationDrawable
//import android.graphics.drawable.Drawable // import android.graphics.drawable.Drawable
//import android.view.View // import android.view.View
//import android.widget.Checkable // import android.widget.Checkable
//import android.widget.CheckedTextView // import android.widget.CheckedTextView
//import android.widget.ImageView // import android.widget.ImageView
//import android.widget.LinearLayout // import android.widget.LinearLayout
//import android.widget.TextView // import android.widget.TextView
//import androidx.core.view.isVisible // import androidx.core.view.isVisible
//import androidx.recyclerview.widget.RecyclerView // import androidx.recyclerview.widget.RecyclerView
//import org.koin.core.component.KoinComponent // import org.koin.core.component.KoinComponent
//import org.koin.core.component.get // import org.koin.core.component.get
//import org.koin.core.component.inject // import org.koin.core.component.inject
//import org.moire.ultrasonic.R // import org.moire.ultrasonic.R
//import org.moire.ultrasonic.data.ActiveServerProvider // import org.moire.ultrasonic.data.ActiveServerProvider
//import org.moire.ultrasonic.domain.MusicDirectory // import org.moire.ultrasonic.domain.MusicDirectory
//import org.moire.ultrasonic.featureflags.Feature // import org.moire.ultrasonic.featureflags.Feature
//import org.moire.ultrasonic.featureflags.FeatureStorage // import org.moire.ultrasonic.featureflags.FeatureStorage
//import org.moire.ultrasonic.fragment.DownloadRowAdapter // import org.moire.ultrasonic.fragment.DownloadRowAdapter
//import org.moire.ultrasonic.service.DownloadFile // import org.moire.ultrasonic.service.DownloadFile
//import org.moire.ultrasonic.service.MediaPlayerController // import org.moire.ultrasonic.service.MediaPlayerController
//import org.moire.ultrasonic.service.MusicServiceFactory // import org.moire.ultrasonic.service.MusicServiceFactory
//import org.moire.ultrasonic.util.Settings // import org.moire.ultrasonic.util.Settings
//import org.moire.ultrasonic.util.Util // import org.moire.ultrasonic.util.Util
//import timber.log.Timber // import timber.log.Timber
// //
///** // /**
// * Used to display songs and videos in a `ListView`. // * Used to display songs and videos in a `ListView`.
// * TODO: Video List item // * TODO: Video List item
// */ // */
//class SongViewHolder(view: View, context: Context) : RecyclerView.ViewHolder(view), Checkable, KoinComponent { // class SongViewHolder(view: View, context: Context) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
// var check: CheckedTextView = view.findViewById(R.id.song_check) // var check: CheckedTextView = view.findViewById(R.id.song_check)
// var rating: LinearLayout = view.findViewById(R.id.song_rating) // var rating: LinearLayout = view.findViewById(R.id.song_rating)
// var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1) // var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
@ -252,42 +252,42 @@
// } // }
// } // }
// //
//// fun updateDownloadStatus2( // // fun updateDownloadStatus2(
//// downloadFile: DownloadFile, // // downloadFile: DownloadFile,
//// ) { // // ) {
//// // //
//// var image: Drawable? = null // // var image: Drawable? = null
//// // //
//// when (downloadFile.status.value) { // // when (downloadFile.status.value) {
//// DownloadStatus.DONE -> { // // DownloadStatus.DONE -> {
//// image = if (downloadFile.isSaved) DownloadRowAdapter.pinImage else DownloadRowAdapter.downloadedImage // // image = if (downloadFile.isSaved) DownloadRowAdapter.pinImage else DownloadRowAdapter.downloadedImage
//// status.text = null // // status.text = null
//// } // // }
//// DownloadStatus.DOWNLOADING -> { // // DownloadStatus.DOWNLOADING -> {
//// status.text = Util.formatPercentage(downloadFile.progress.value!!) // // status.text = Util.formatPercentage(downloadFile.progress.value!!)
//// image = DownloadRowAdapter.downloadingImage // // image = DownloadRowAdapter.downloadingImage
//// } // // }
//// else -> { // // else -> {
//// status.text = null // // status.text = null
//// } // // }
//// } // // }
//// // //
//// // TODO: Migrate the image animation stuff from SongView into this class // // // TODO: Migrate the image animation stuff from SongView into this class
//// // //
//// if (image != null) { // // if (image != null) {
//// status.setCompoundDrawablesWithIntrinsicBounds( // // status.setCompoundDrawablesWithIntrinsicBounds(
//// image, null, null, null // // image, null, null, null
//// ) // // )
//// } // // }
//// // //
//// if (image === DownloadRowAdapter.downloadingImage) { // // if (image === DownloadRowAdapter.downloadingImage) {
//// // FIXME // // // FIXME
////// val frameAnimation = image as AnimationDrawable // //// val frameAnimation = image as AnimationDrawable
////// // ////
////// frameAnimation.setVisible(true, true) // //// frameAnimation.setVisible(true, true)
////// frameAnimation.start() // //// frameAnimation.start()
//// } // // }
//// } // // }
// //
// override fun setChecked(newStatus: Boolean) { // override fun setChecked(newStatus: Boolean) {
// check.isChecked = newStatus // check.isChecked = newStatus
@ -313,4 +313,4 @@
// } // }
// //
// //
//} // }

View File

@ -2,31 +2,22 @@
<LinearLayout <LinearLayout
xmlns:a="http://schemas.android.com/apk/res/android" xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical" a:orientation="vertical"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="fill_parent"> a:layout_height="fill_parent">
<TextView <TextView
a:id="@+id/playlist_empty" a:id="@+id/playlist_empty"
a:text="@string/download.empty" a:text="@string/playlist.empty"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="wrap_content" a:layout_height="wrap_content"
a:padding="10dip"/> a:padding="10dip"/>
<com.mobeta.android.dslv.DragSortListView <androidx.recyclerview.widget.RecyclerView
a:id="@+id/playlist_view" a:id="@+id/playlist_view"
a:layout_width="fill_parent" a:layout_width="fill_parent"
a:layout_height="0dip" a:layout_height="0dip"
a:layout_weight="1" a:layout_weight="1"
a:fastScrollEnabled="true" a:fastScrollEnabled="true" />
a:textFilterEnabled="true"
app:drag_handle_id="@+id/song_drag"
app:remove_enabled="true"
app:remove_mode="flingRemove"
app:fling_handle_id="@+id/song_drag"
app:drag_start_mode="onMove"
app:float_background_color="?attr/color_background"
app:float_alpha="0.7" />
</LinearLayout> </LinearLayout>

View File

@ -15,7 +15,8 @@
a:background="@android:color/transparent" a:background="@android:color/transparent"
a:focusable="false" a:focusable="false"
a:gravity="center_vertical" a:gravity="center_vertical"
a:src="?attr/drag_vertical" /> a:src="?attr/drag_vertical"
a:importantForAccessibility="no" />
<CheckedTextView <CheckedTextView
a:id="@+id/song_check" a:id="@+id/song_check"
@ -96,6 +97,7 @@
a:focusable="false" a:focusable="false"
a:gravity="center_vertical" a:gravity="center_vertical"
a:paddingEnd="8dip" a:paddingEnd="8dip"
a:src="?attr/star_hollow" /> a:src="?attr/star_hollow"
a:contentDescription="@string/download.menu_star"/>
</LinearLayout> </LinearLayout>

View File

@ -43,7 +43,7 @@
<string name="delete_playlist">Opravdu smazat %1$s</string> <string name="delete_playlist">Opravdu smazat %1$s</string>
<string name="download.bookmark_removed" formatted="false">Záložka odstraněna.</string> <string name="download.bookmark_removed" formatted="false">Záložka odstraněna.</string>
<string name="download.bookmark_set_at_position" formatted="false">Záložka vytvořena na %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Záložka vytvořena na %s.</string>
<string name="download.empty">Playlist je prázdný</string> <string name="playlist.empty">Playlist je prázdný</string>
<string name="download.jukebox_not_authorized">Vzdálené ovládání není povoleno. Povolte jukebox mód v <b>Uživatelském &gt; Nastavení</b> na Subsonic serveru.</string> <string name="download.jukebox_not_authorized">Vzdálené ovládání není povoleno. Povolte jukebox mód v <b>Uživatelském &gt; Nastavení</b> na Subsonic serveru.</string>
<string name="download.jukebox_off">Vzdálené ovládání vypnuto. Hudba je přehrávána na telefonu.</string> <string name="download.jukebox_off">Vzdálené ovládání vypnuto. Hudba je přehrávána na telefonu.</string>
<string name="download.jukebox_offline">Vzdálené ovládání není dostupné v offline módu.</string> <string name="download.jukebox_offline">Vzdálené ovládání není dostupné v offline módu.</string>

View File

@ -42,7 +42,7 @@
<string name="delete_playlist">Möchtest du %1$s löschen</string> <string name="delete_playlist">Möchtest du %1$s löschen</string>
<string name="download.bookmark_removed" formatted="false">Lesezeichen entfernt</string> <string name="download.bookmark_removed" formatted="false">Lesezeichen entfernt</string>
<string name="download.bookmark_set_at_position" formatted="false">Lesezeichen gesetzt als %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Lesezeichen gesetzt als %s.</string>
<string name="download.empty">Wiedergabeliste ist leer</string> <string name="playlist.empty">Wiedergabeliste ist leer</string>
<string name="download.jukebox_not_authorized">Fernbedienung ist nicht erlaubt. Bitte Jukebox Modus auf dem Subsonic Server in <b>Benutzer > Einstellungen</b> aktivieren.</string> <string name="download.jukebox_not_authorized">Fernbedienung ist nicht erlaubt. Bitte Jukebox Modus auf dem Subsonic Server in <b>Benutzer > Einstellungen</b> aktivieren.</string>
<string name="download.jukebox_off">Fernbedienung ausgeschaltet. Musik wird auf dem Telefon wiedergegeben.</string> <string name="download.jukebox_off">Fernbedienung ausgeschaltet. Musik wird auf dem Telefon wiedergegeben.</string>
<string name="download.jukebox_offline">Fernbedienungs-Modus is Offline nicht verfügbar.</string> <string name="download.jukebox_offline">Fernbedienungs-Modus is Offline nicht verfügbar.</string>

View File

@ -56,7 +56,7 @@
<string name="delete_playlist">Quieres eliminar %1$s</string> <string name="delete_playlist">Quieres eliminar %1$s</string>
<string name="download.bookmark_removed" formatted="false">Marcador eliminado.</string> <string name="download.bookmark_removed" formatted="false">Marcador eliminado.</string>
<string name="download.bookmark_set_at_position" formatted="false">Marcador añadido a %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Marcador añadido a %s.</string>
<string name="download.empty">La lista de reproducción esta vacía</string> <string name="playlist.empty">La lista de reproducción esta vacía</string>
<string name="download.jukebox_not_authorized">El control remoto no esta habilitado. Por favor habilita el modo jukebox en <b>Configuración &gt; Usuarios</b> en tu servidor de Subsonic.</string> <string name="download.jukebox_not_authorized">El control remoto no esta habilitado. Por favor habilita el modo jukebox en <b>Configuración &gt; Usuarios</b> en tu servidor de Subsonic.</string>
<string name="download.jukebox_off">Control remoto apagado. La música se reproduce en tu dispositivo.</string> <string name="download.jukebox_off">Control remoto apagado. La música se reproduce en tu dispositivo.</string>
<string name="download.jukebox_offline">Control remoto no disponible en modo fuera de línea.</string> <string name="download.jukebox_offline">Control remoto no disponible en modo fuera de línea.</string>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">Voulez-vous supprimer %1$s</string> <string name="delete_playlist">Voulez-vous supprimer %1$s</string>
<string name="download.bookmark_removed" formatted="false">Signet supprimé.</string> <string name="download.bookmark_removed" formatted="false">Signet supprimé.</string>
<string name="download.bookmark_set_at_position" formatted="false">Signet ajouté à %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Signet ajouté à %s.</string>
<string name="download.empty">La playlist est vide</string> <string name="playlist.empty">La playlist est vide</string>
<string name="download.jukebox_not_authorized">La télécommande n\'est pas autorisée. Veuillez activer le mode jukebox dans <b>Utilisateurs &gt; Paramètres</b> à partir de votre serveur Subsonic.</string> <string name="download.jukebox_not_authorized">La télécommande n\'est pas autorisée. Veuillez activer le mode jukebox dans <b>Utilisateurs &gt; Paramètres</b> à partir de votre serveur Subsonic.</string>
<string name="download.jukebox_off">Mode jukebox désactivé. La musique est jouée sur l\'appareil.</string> <string name="download.jukebox_off">Mode jukebox désactivé. La musique est jouée sur l\'appareil.</string>
<string name="download.jukebox_offline">Le mode jukebox n\'est pas disponible en mode déconnecté.</string> <string name="download.jukebox_offline">Le mode jukebox n\'est pas disponible en mode déconnecté.</string>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">Biztos, hogy törölni akarja? %1$s</string> <string name="delete_playlist">Biztos, hogy törölni akarja? %1$s</string>
<string name="download.bookmark_removed" formatted="false">Könyvjelző eltávolítva.</string> <string name="download.bookmark_removed" formatted="false">Könyvjelző eltávolítva.</string>
<string name="download.bookmark_set_at_position" formatted="false">Könyvjelző beállítva %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Könyvjelző beállítva %s.</string>
<string name="download.empty">A várólista üres!</string> <string name="playlist.empty">A várólista üres!</string>
<string name="download.jukebox_not_authorized">A távvezérlés nem áll rendelkezésre. Kérjük, engedélyezze a Jukebox módot a <b>Felhasználók &gt; Beállítások</b> menüpontban, az Ön Subsonic kiszolgálóján!</string> <string name="download.jukebox_not_authorized">A távvezérlés nem áll rendelkezésre. Kérjük, engedélyezze a Jukebox módot a <b>Felhasználók &gt; Beállítások</b> menüpontban, az Ön Subsonic kiszolgálóján!</string>
<string name="download.jukebox_off">Távvezérlés kikapcsolása. A zenelejátszás a telefonon történik.</string> <string name="download.jukebox_off">Távvezérlés kikapcsolása. A zenelejátszás a telefonon történik.</string>
<string name="download.jukebox_offline">A távvezérlés nem lehetséges kapcsolat nélküli módban!</string> <string name="download.jukebox_offline">A távvezérlés nem lehetséges kapcsolat nélküli módban!</string>

View File

@ -40,7 +40,7 @@
<string name="delete_playlist">Vuoi eliminare %1$s</string> <string name="delete_playlist">Vuoi eliminare %1$s</string>
<string name="download.bookmark_removed" formatted="false">Segnalibro rimosso.</string> <string name="download.bookmark_removed" formatted="false">Segnalibro rimosso.</string>
<string name="download.bookmark_set_at_position" formatted="false">Segnalibro impostato su %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Segnalibro impostato su %s.</string>
<string name="download.empty">Playlist vuota</string> <string name="playlist.empty">Playlist vuota</string>
<string name="download.jukebox_not_authorized">Il controllo remoto non è consentito. Per favore abilita la modalità jukebox nelle <b> Impostazioni &gt; Utente </b> nel server Airsonic.</string> <string name="download.jukebox_not_authorized">Il controllo remoto non è consentito. Per favore abilita la modalità jukebox nelle <b> Impostazioni &gt; Utente </b> nel server Airsonic.</string>
<string name="download.jukebox_off">Controllo remoto disattivato. La musica verrà riprodotta sullo smartphone.</string> <string name="download.jukebox_off">Controllo remoto disattivato. La musica verrà riprodotta sullo smartphone.</string>
<string name="download.jukebox_offline">Il controllo remoto non è disponibile nella modalità offline. </string> <string name="download.jukebox_offline">Il controllo remoto non è disponibile nella modalità offline. </string>

View File

@ -56,7 +56,7 @@
<string name="delete_playlist">Wil je %1$s verwijderen?</string> <string name="delete_playlist">Wil je %1$s verwijderen?</string>
<string name="download.bookmark_removed" formatted="false">Bladwijzer verwijderd.</string> <string name="download.bookmark_removed" formatted="false">Bladwijzer verwijderd.</string>
<string name="download.bookmark_set_at_position" formatted="false">Bladwijzer ingesteld op %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Bladwijzer ingesteld op %s.</string>
<string name="download.empty">Lege afspeellijst</string> <string name="playlist.empty">Lege afspeellijst</string>
<string name="download.jukebox_not_authorized">Afstandsbediening wordt niet ondersteund. Schakel jukebox-modus in op je Subsonic-server via <b>Gebruikers &gt; Instellingen</b>.</string> <string name="download.jukebox_not_authorized">Afstandsbediening wordt niet ondersteund. Schakel jukebox-modus in op je Subsonic-server via <b>Gebruikers &gt; Instellingen</b>.</string>
<string name="download.jukebox_off">Afstandsbediening uitgeschakeld; muziek wordt afgespeeld op de telefoon.</string> <string name="download.jukebox_off">Afstandsbediening uitgeschakeld; muziek wordt afgespeeld op de telefoon.</string>
<string name="download.jukebox_offline">Afstandsbediening is niet beschikbaar in offline-modus.</string> <string name="download.jukebox_offline">Afstandsbediening is niet beschikbaar in offline-modus.</string>

View File

@ -42,7 +42,7 @@
<string name="delete_playlist">Czy chcesz usunąć %1$s?</string> <string name="delete_playlist">Czy chcesz usunąć %1$s?</string>
<string name="download.bookmark_removed" formatted="false">Zakładka usunięta.</string> <string name="download.bookmark_removed" formatted="false">Zakładka usunięta.</string>
<string name="download.bookmark_set_at_position" formatted="false">Zakładka ustawiona na %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Zakładka ustawiona na %s.</string>
<string name="download.empty">Playlista jest pusta</string> <string name="playlist.empty">Playlista jest pusta</string>
<string name="download.jukebox_not_authorized">Kontrola pilotem jest niedostępna. Proszę uruchomić tryb jukebox w <b>Użytkownicy &gt; Ustawienia</b> na serwerze Subsonic.</string> <string name="download.jukebox_not_authorized">Kontrola pilotem jest niedostępna. Proszę uruchomić tryb jukebox w <b>Użytkownicy &gt; Ustawienia</b> na serwerze Subsonic.</string>
<string name="download.jukebox_off">Tryb pilota jest wyłączony. Muzyka jest odtwarzana w telefonie.</string> <string name="download.jukebox_off">Tryb pilota jest wyłączony. Muzyka jest odtwarzana w telefonie.</string>
<string name="download.jukebox_offline">Pilot jest niedostępny w trybie offline.</string> <string name="download.jukebox_offline">Pilot jest niedostępny w trybie offline.</string>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">Você quer excluir %1$s</string> <string name="delete_playlist">Você quer excluir %1$s</string>
<string name="download.bookmark_removed" formatted="false">Favorito removido.</string> <string name="download.bookmark_removed" formatted="false">Favorito removido.</string>
<string name="download.bookmark_set_at_position" formatted="false">Favorito marcado em %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Favorito marcado em %s.</string>
<string name="download.empty">Playlist está vazia</string> <string name="playlist.empty">Playlist está vazia</string>
<string name="download.jukebox_not_authorized">Controle remoto não está permitido. Habilite o modo jukebox em <b>Usuário &gt; Configurações</b> no seu servidor Subsonic.</string> <string name="download.jukebox_not_authorized">Controle remoto não está permitido. Habilite o modo jukebox em <b>Usuário &gt; Configurações</b> no seu servidor Subsonic.</string>
<string name="download.jukebox_off">Controle remoto desligado. Música tocada no celular.</string> <string name="download.jukebox_off">Controle remoto desligado. Música tocada no celular.</string>
<string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string> <string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string>

View File

@ -42,7 +42,7 @@
<string name="delete_playlist">Você quer apagar %1$s</string> <string name="delete_playlist">Você quer apagar %1$s</string>
<string name="download.bookmark_removed" formatted="false">Favorito removido.</string> <string name="download.bookmark_removed" formatted="false">Favorito removido.</string>
<string name="download.bookmark_set_at_position" formatted="false">Favorito marcado em %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Favorito marcado em %s.</string>
<string name="download.empty">Playlist está vazia</string> <string name="playlist.empty">Playlist está vazia</string>
<string name="download.jukebox_not_authorized">Controle remoto não está permitido. Habilite o modo jukebox em <b>Usuário &gt; Configurações</b> no seu servidor Subsonic.</string> <string name="download.jukebox_not_authorized">Controle remoto não está permitido. Habilite o modo jukebox em <b>Usuário &gt; Configurações</b> no seu servidor Subsonic.</string>
<string name="download.jukebox_off">Controle remoto desligado. Música tocada no celular.</string> <string name="download.jukebox_off">Controle remoto desligado. Música tocada no celular.</string>
<string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string> <string name="download.jukebox_offline">Controle remoto não está disponível no modo offline.</string>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">Вы хотите удалить %1$s</string> <string name="delete_playlist">Вы хотите удалить %1$s</string>
<string name="download.bookmark_removed" formatted="false">Закладка удалена</string> <string name="download.bookmark_removed" formatted="false">Закладка удалена</string>
<string name="download.bookmark_set_at_position" formatted="false">Закладка установлена ​​на %s</string> <string name="download.bookmark_set_at_position" formatted="false">Закладка установлена ​​на %s</string>
<string name="download.empty">Плейлист пустой</string> <string name="playlist.empty">Плейлист пустой</string>
<string name="download.jukebox_not_authorized">Пульт дистанционного управления не допускается. Пожалуйста, включите режим музыкального автомата в <b> Пользователи &gt; Настройки </b>на вашем Subsonic сервере.</string> <string name="download.jukebox_not_authorized">Пульт дистанционного управления не допускается. Пожалуйста, включите режим музыкального автомата в <b> Пользователи &gt; Настройки </b>на вашем Subsonic сервере.</string>
<string name="download.jukebox_off">Пульт управления выключен. Музыка играет на телефоне</string> <string name="download.jukebox_off">Пульт управления выключен. Музыка играет на телефоне</string>
<string name="download.jukebox_offline">Пульт дистанционного управления недоступен в автономном режиме.</string> <string name="download.jukebox_offline">Пульт дистанционного управления недоступен в автономном режиме.</string>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">确定要删除 %1$s吗</string> <string name="delete_playlist">确定要删除 %1$s吗</string>
<string name="download.bookmark_removed" formatted="false">书签已删除。</string> <string name="download.bookmark_removed" formatted="false">书签已删除。</string>
<string name="download.bookmark_set_at_position" formatted="false">书签设置为 %s。</string> <string name="download.bookmark_set_at_position" formatted="false">书签设置为 %s。</string>
<string name="download.empty">空的播放列表</string> <string name="playlist.empty">空的播放列表</string>
<string name="download.jukebox_not_authorized">不允许远程控制. 请在您的服务器上的 <b>Users &gt; Settings</b> 打开点唱机模式。</string> <string name="download.jukebox_not_authorized">不允许远程控制. 请在您的服务器上的 <b>Users &gt; Settings</b> 打开点唱机模式。</string>
<string name="download.jukebox_off">关闭远程控制,音乐将在手机上播放</string> <string name="download.jukebox_off">关闭远程控制,音乐将在手机上播放</string>
<string name="download.jukebox_offline">离线模式不支持远程控制</string> <string name="download.jukebox_offline">离线模式不支持远程控制</string>

View File

@ -56,7 +56,7 @@
<string name="delete_playlist">Do you want to delete %1$s</string> <string name="delete_playlist">Do you want to delete %1$s</string>
<string name="download.bookmark_removed" formatted="false">Bookmark removed.</string> <string name="download.bookmark_removed" formatted="false">Bookmark removed.</string>
<string name="download.bookmark_set_at_position" formatted="false">Bookmark set at %s.</string> <string name="download.bookmark_set_at_position" formatted="false">Bookmark set at %s.</string>
<string name="download.empty">Playlist is empty</string> <string name="playlist.empty">Playlist is empty</string>
<string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string> <string name="download.jukebox_not_authorized">Remote control is not allowed. Please enable jukebox mode in <b>Users &gt; Settings</b> on your Subsonic server.</string>
<string name="download.jukebox_off">Turned off remote control. Music is played on phone.</string> <string name="download.jukebox_off">Turned off remote control. Music is played on phone.</string>
<string name="download.jukebox_offline">Remote control is not available in offline mode.</string> <string name="download.jukebox_offline">Remote control is not available in offline mode.</string>
@ -464,6 +464,10 @@
<item quantity="one">%d song unpinned</item> <item quantity="one">%d song unpinned</item>
<item quantity="other">%d songs unpinned</item> <item quantity="other">%d songs unpinned</item>
</plurals> </plurals>
<plurals name="select_album_n_songs_deleted">
<item quantity="one">%d song deleted</item>
<item quantity="other">%d songs deleted</item>
</plurals>
<plurals name="select_album_n_songs_added"> <plurals name="select_album_n_songs_added">
<item quantity="one">%d song added to the end of play queue</item> <item quantity="one">%d song added to the end of play queue</item>
<item quantity="other">%d songs added to the end of play queue</item> <item quantity="other">%d songs added to the end of play queue</item>