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

View File

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

View File

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

View File

@ -37,6 +37,8 @@ import org.moire.ultrasonic.util.Util;
*
* @author Sindre Mehus
*/
public class AlbumView extends UpdateView
{
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 androidx.recyclerview.widget.RecyclerView
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.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
import org.moire.ultrasonic.util.AlbumHeader
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
@ -51,7 +50,6 @@ class HeaderViewBinder(
val context = weakContext.get() ?: return
val resources = context.resources
val artworkSelection = random.nextInt(item.childCount)
imageLoaderProvider.getImageLoader().loadImage(
@ -61,7 +59,6 @@ class HeaderViewBinder(
holder.titleView.text = item.name
// Don't show a header if all entries are videos
if (item.isAllVideo) {
return
@ -74,7 +71,6 @@ class HeaderViewBinder(
}
holder.artistView.text = artist
val genre: String = if (item.genres.size == 1) {
item.genres.iterator().next()
} else {
@ -83,7 +79,6 @@ class HeaderViewBinder(
holder.genreView.text = genre
val year: String = if (item.years.size == 1) {
item.years.iterator().next().toString()
} else {
@ -92,7 +87,6 @@ class HeaderViewBinder(
holder.yearView.text = year
val songs = resources.getQuantityString(
R.plurals.select_album_n_songs, item.childCount,
item.childCount

View File

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

View File

@ -8,8 +8,8 @@ import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.AsyncListDiffer.ListListener
import androidx.recyclerview.widget.DiffUtil
import com.drakeet.multitype.MultiTypeAdapter
import org.moire.ultrasonic.domain.Identifiable
import java.util.TreeSet
import org.moire.ultrasonic.domain.Identifiable
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")
}
var mDiffer: AsyncListDiffer<T> = AsyncListDiffer(
AdapterListUpdateCallback(this),
AsyncDifferConfig.Builder(diffCallback).build()
@ -54,7 +53,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
mDiffer.addListListener(mListener)
}
/**
* Submits a new list to be diffed, and displayed.
*
@ -88,8 +86,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
mDiffer.submitList(list, commitCallback)
}
override fun getItemCount(): Int {
return mDiffer.currentList.size
}
@ -151,7 +147,6 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
selectionRevision.postValue(selectionRevision.value!! + 1)
}
fun setSelectionStatusOfAll(select: Boolean): Int {
// Clear current selection
selectedSet.clear()
@ -163,10 +158,13 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
if (!select) return 0
// Select them all
getCurrentList().mapNotNullTo(selectedSet, { entry ->
// Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
})
getCurrentList().mapNotNullTo(
selectedSet,
{ entry ->
// Exclude any -1 ids, eg. headers and other UI elements
entry.longId.takeIf { it != -1L }
}
)
return selectedSet.count()
}
@ -175,6 +173,18 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
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 {
/**
@ -190,8 +200,5 @@ class MultiTypeDiffAdapter<T : Identifiable> : MultiTypeAdapter() {
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.ServerSetting
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.fragment.ServerSettingsModel
import org.moire.ultrasonic.util.Util
/**

View File

@ -2,6 +2,7 @@ package org.moire.ultrasonic.adapters
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
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.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
import timber.log.Timber
class TrackViewBinder(
val checkable: Boolean,
val draggable: Boolean,
context: Context,
val lifecycleOwner: LifecycleOwner
val lifecycleOwner: LifecycleOwner,
private val onClickCallback: ((View, DownloadFile?) -> Unit)? = null
) : ItemViewBinder<Identifiable, TrackViewHolder>(), KoinComponent {
// //
// onItemClick: (MusicDirectory.Entry) -> Unit,
// onContextMenuClick: (MenuItem, MusicDirectory.Entry) -> Boolean,
@ -40,12 +40,13 @@ class TrackViewBinder(
private val imageHelper: ImageHelper = ImageHelper(context)
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) {
val downloadFile: DownloadFile?
val _adapter = adapter as MultiTypeDiffAdapter<*>
when (item) {
is MusicDirectory.Entry -> {
@ -65,33 +66,47 @@ class TrackViewBinder(
file = downloadFile,
checkable = checkable,
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
holder.adapter.selectionRevision.observe(lifecycleOwner, {
val newStatus = holder.adapter.isSelected(item.longId)
_adapter.selectionRevision.observe(
lifecycleOwner,
{
val newStatus = _adapter.isSelected(item.longId)
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()
if (newStatus != holder.check.isChecked) holder.check.isChecked = newStatus
}
)
downloadFile.progress.observe(lifecycleOwner, {
Timber.w("CAUGHT PROGRESS CHANGE")
// Observe download status
downloadFile.status.observe(
lifecycleOwner,
{
holder.updateStatus(it)
_adapter.notifyChanged()
}
)
downloadFile.progress.observe(
lifecycleOwner,
{
holder.updateProgress(it)
}
)
holder.itemClickListener = onClickCallback
}
}

View File

@ -9,13 +9,13 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.domain.MusicDirectory
import org.moire.ultrasonic.featureflags.Feature
import org.moire.ultrasonic.featureflags.FeatureStorage
@ -31,8 +31,7 @@ import timber.log.Timber
* Used to display songs and videos in a `ListView`.
* TODO: Video List item
*/
class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifiable>) :
RecyclerView.ViewHolder(view), Checkable, KoinComponent {
class TrackViewHolder(val view: View) : RecyclerView.ViewHolder(view), Checkable, KoinComponent {
var check: CheckedTextView = view.findViewById(R.id.song_check)
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 progress: TextView = view.findViewById(R.id.song_status)
var itemClickListener: ((View, DownloadFile?) -> Unit)? = null
var entry: MusicDirectory.Entry? = null
private set
var downloadFile: DownloadFile? = null
@ -59,6 +60,8 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
private var statusImage: Drawable? = null
private var playing = false
var observableChecked = MutableLiveData(false)
private val useFiveStarRating: Boolean by lazy {
val features: FeatureStorage = get()
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()
lateinit var imageHelper: ImageHelper
init {
itemView.setOnClickListener {
val nowChecked = !check.isChecked
isChecked = nowChecked
if (itemClickListener != null) {
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
duration.text = entryDescription.duration
if (Settings.shouldShowTrackNumber && song.track != null && song.track!! > 0) {
track.text = entryDescription.trackNumber
} else {
@ -100,7 +106,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
}
check.isVisible = (checkable && !song.isVideo)
check.isChecked = isSelected
setCheckedSilent(isSelected)
drag.isVisible = draggable
if (ActiveServerProvider.isOffline()) {
@ -109,7 +115,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
} else {
setupStarButtons(song)
}
update()
}
@ -151,9 +157,6 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
}
}
@Synchronized
// TODO: Should be removed
fun update() {
@ -218,12 +221,10 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
}
}
fun updateStatus(status: DownloadStatus) {
if (status == cachedStatus) return
cachedStatus = status
Timber.w("STATUS: %s", status)
when (status) {
@ -254,7 +255,7 @@ class TrackViewHolder(val view: View, var adapter: MultiTypeDiffAdapter<Identifi
fun updateProgress(p: Int) {
if (cachedStatus == DownloadStatus.DOWNLOADING) {
progress.text = Util.formatPercentage(p)
} else {
} else {
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) {
if (newStatus) {
adapter.notifySelected(downloadFile!!.longId)
} else {
adapter.notifyUnselected(downloadFile!!.longId)
}
observableChecked.postValue(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 org.koin.core.component.inject
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
import org.moire.ultrasonic.adapters.TrackViewBinder
import org.moire.ultrasonic.domain.Identifiable
import org.moire.ultrasonic.service.DownloadFile
import org.moire.ultrasonic.service.Downloader
import org.moire.ultrasonic.util.Util
@ -54,7 +52,7 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
viewAdapter.register(
TrackViewBinder(
checkable = true,
checkable = false,
draggable = false,
context = requireContext(),
lifecycleOwner = viewLifecycleOwner
@ -65,7 +63,6 @@ class DownloadsFragment : MultiListFragment<DownloadFile>() {
}
}
class DownloadListModel(application: Application) : GenericListModel(application) {
private val downloader by inject<Downloader>()
@ -73,6 +70,3 @@ class DownloadListModel(application: Application) : GenericListModel(application
return downloader.observableDownloads
}
}

View File

@ -8,18 +8,14 @@ import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.LiveData
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.drakeet.multitype.MultiTypeAdapter
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.R
import org.moire.ultrasonic.adapters.MultiTypeDiffAdapter
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.MusicFolder
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
* @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() {
internal val activeServerProvider: ActiveServerProvider by inject()
@ -94,7 +89,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
*/
@Suppress("CommentOverPrivateProperty")
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 {
return listModel.showSelectFolderHeader(arguments) &&
!listModel.isOffline() && !Settings.shouldUseId3Tags
!listModel.isOffline() && !Settings.shouldUseId3Tags
}
open fun setTitle(title: String?) {
@ -147,9 +142,13 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
liveDataItems = getLiveData(arguments)
// Register an observer to update our UI when the data changes
liveDataItems.observe(viewLifecycleOwner, {
newItems -> viewAdapter.submitList(newItems)
})
liveDataItems.observe(
viewLifecycleOwner,
{
newItems ->
viewAdapter.submitList(newItems)
}
)
// Setup the Music folder handling
listModel.getMusicFolders().observe(viewLifecycleOwner, musicFolderObserver)
@ -165,7 +164,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
}
// Configure whether to show the folder header
//viewAdapter.folderHeaderEnabled = showFolderHeader()
// viewAdapter.folderHeaderEnabled = showFolderHeader()
}
@Override
@ -187,7 +186,7 @@ abstract class MultiListFragment<T : Identifiable> : Fragment() {
abstract fun onItemClick(item: T)
}
//abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> :
// abstract class EntryListFragment<T : GenericEntry, TA : GenericRowAdapter<T>> :
// GenericListFragment<T, TA>() {
// @Suppress("LongMethod")
// 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))
// findNavController().navigate(itemClickTarget, bundle)
// }
//}
// }

View File

@ -36,8 +36,10 @@ import android.widget.ViewFlipper
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import com.mobeta.android.dslv.DragSortListView
import com.mobeta.android.dslv.DragSortListView.DragSortListener
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.rxjava3.disposables.Disposable
import java.text.DateFormat
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.get
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.VisualizerController
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.PlayerState
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.Util
import org.moire.ultrasonic.view.AutoRepeatButton
import org.moire.ultrasonic.view.SongListAdapter
import org.moire.ultrasonic.view.VisualizerView
import timber.log.Timber
@ -94,6 +98,8 @@ class PlayerFragment :
GestureDetector.OnGestureListener,
KoinComponent,
CoroutineScope by CoroutineScope(Dispatchers.Main) {
// Settings
private var swipeDistance = 0
private var swipeVelocity = 0
private var jukeboxAvailable = false
@ -104,6 +110,7 @@ class PlayerFragment :
// Detectors & Callbacks
private lateinit var gestureScanner: GestureDetector
private lateinit var cancellationToken: CancellationToken
private lateinit var dragTouchHelper: ItemTouchHelper
// Data & Services
private val networkAndStorageChecker: NetworkAndStorageChecker by inject()
@ -114,6 +121,7 @@ class PlayerFragment :
private lateinit var executorService: ScheduledExecutorService
private var currentPlaying: DownloadFile? = null
private var currentSong: MusicDirectory.Entry? = null
private lateinit var viewManager: LinearLayoutManager
private var rxBusSubscription: Disposable? = null
private var ioScope = CoroutineScope(Dispatchers.IO)
@ -133,7 +141,7 @@ class PlayerFragment :
private lateinit var albumTextView: TextView
private lateinit var artistTextView: TextView
private lateinit var albumArtImageView: ImageView
private lateinit var playlistView: DragSortListView
private lateinit var playlistView: RecyclerView
private lateinit var positionTextView: TextView
private lateinit var downloadTrackTextView: TextView
private lateinit var downloadTotalDurationTextView: TextView
@ -146,6 +154,10 @@ class PlayerFragment :
private lateinit var fullStar: Drawable
private lateinit var progressBar: SeekBar
internal val viewAdapter: MultiTypeDiffAdapter<Identifiable> by lazy {
MultiTypeDiffAdapter()
}
override fun onCreate(savedInstanceState: Bundle?) {
Util.applyTheme(this.context)
super.onCreate(savedInstanceState)
@ -322,14 +334,7 @@ class PlayerFragment :
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
})
playlistView.setOnItemClickListener { _, _, position, _ ->
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
launch(CommunicationError.getHandler(context)) {
mediaPlayerController.play(position)
onCurrentChanged()
onSliderProgressChanged()
}
}
initPlaylistDisplay()
registerForContextMenu(playlistView)
@ -432,15 +437,12 @@ class PlayerFragment :
// Scroll to current playing.
private fun scrollToCurrent() {
val adapter = playlistView.adapter
if (adapter != null) {
val count = adapter.count
for (i in 0 until count) {
if (currentPlaying == playlistView.getItemAtPosition(i)) {
playlistView.smoothScrollToPositionFromTop(i, 40)
return
}
}
val index = mediaPlayerController.playList.indexOf(currentPlaying)
if (index != -1) {
val smoothScroller = LinearSmoothScroller(context)
smoothScroller.targetPosition = index
viewManager.startSmoothScroll(smoothScroller)
}
}
@ -535,7 +537,7 @@ class PlayerFragment :
super.onCreateContextMenu(menu, view, menuInfo)
if (view === playlistView) {
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
menuInflater.inflate(R.menu.nowplaying_context, menu)
val song: MusicDirectory.Entry?
@ -561,7 +563,7 @@ class PlayerFragment :
override fun onContextItemSelected(menuItem: MenuItem): Boolean {
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(
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() {
val mediaPlayerController = mediaPlayerController
val list = mediaPlayerController.playList
emptyTextView.setText(R.string.download_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()
}
}
emptyTextView.setText(R.string.playlist_empty)
override fun drag(from: Int, to: Int) {}
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()
}
})
viewAdapter.submitList(list)
emptyTextView.isVisible = list.isEmpty()

View File

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

View File

@ -13,11 +13,8 @@ import androidx.lifecycle.MutableLiveData
import java.util.LinkedList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.inject
import org.moire.ultrasonic.R
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.MusicServiceFactory
import org.moire.ultrasonic.util.Settings

View File

@ -89,7 +89,6 @@ class DownloadFile(
}
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?,
)
fun getMediaDescriptionForEntry(
song: MusicDirectory.Entry,
mediaId: String? = null,
@ -921,8 +920,8 @@ object Util {
if (artistName != null) {
if (Settings.shouldDisplayBitrateWithArtist && (
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
)
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
)
) {
artist.append(artistName).append(" (").append(
String.format(
@ -939,7 +938,6 @@ object Util {
val trackNumber = song.track ?: 0
val title = StringBuilder(LINE_LENGTH)
if (Settings.shouldShowTrackNumber && trackNumber > 0) {
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.graphics.drawable.AnimationDrawable
//import android.graphics.drawable.Drawable
//import android.view.View
//import android.widget.Checkable
//import android.widget.CheckedTextView
//import android.widget.ImageView
//import android.widget.LinearLayout
//import android.widget.TextView
//import androidx.core.view.isVisible
//import androidx.recyclerview.widget.RecyclerView
//import org.koin.core.component.KoinComponent
//import org.koin.core.component.get
//import org.koin.core.component.inject
//import org.moire.ultrasonic.R
//import org.moire.ultrasonic.data.ActiveServerProvider
//import org.moire.ultrasonic.domain.MusicDirectory
//import org.moire.ultrasonic.featureflags.Feature
//import org.moire.ultrasonic.featureflags.FeatureStorage
//import org.moire.ultrasonic.fragment.DownloadRowAdapter
//import org.moire.ultrasonic.service.DownloadFile
//import org.moire.ultrasonic.service.MediaPlayerController
//import org.moire.ultrasonic.service.MusicServiceFactory
//import org.moire.ultrasonic.util.Settings
//import org.moire.ultrasonic.util.Util
//import timber.log.Timber
// import android.content.Context
// import android.graphics.drawable.AnimationDrawable
// import android.graphics.drawable.Drawable
// import android.view.View
// import android.widget.Checkable
// import android.widget.CheckedTextView
// import android.widget.ImageView
// import android.widget.LinearLayout
// import android.widget.TextView
// import androidx.core.view.isVisible
// import androidx.recyclerview.widget.RecyclerView
// import org.koin.core.component.KoinComponent
// import org.koin.core.component.get
// import org.koin.core.component.inject
// import org.moire.ultrasonic.R
// import org.moire.ultrasonic.data.ActiveServerProvider
// import org.moire.ultrasonic.domain.MusicDirectory
// import org.moire.ultrasonic.featureflags.Feature
// import org.moire.ultrasonic.featureflags.FeatureStorage
// import org.moire.ultrasonic.fragment.DownloadRowAdapter
// import org.moire.ultrasonic.service.DownloadFile
// import org.moire.ultrasonic.service.MediaPlayerController
// import org.moire.ultrasonic.service.MusicServiceFactory
// import org.moire.ultrasonic.util.Settings
// import org.moire.ultrasonic.util.Util
// import timber.log.Timber
//
///**
// /**
// * Used to display songs and videos in a `ListView`.
// * 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 rating: LinearLayout = view.findViewById(R.id.song_rating)
// var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
@ -252,42 +252,42 @@
// }
// }
//
//// fun updateDownloadStatus2(
//// downloadFile: DownloadFile,
//// ) {
////
//// var image: Drawable? = null
////
//// when (downloadFile.status.value) {
//// DownloadStatus.DONE -> {
//// image = if (downloadFile.isSaved) DownloadRowAdapter.pinImage else DownloadRowAdapter.downloadedImage
//// status.text = null
//// }
//// DownloadStatus.DOWNLOADING -> {
//// status.text = Util.formatPercentage(downloadFile.progress.value!!)
//// image = DownloadRowAdapter.downloadingImage
//// }
//// else -> {
//// status.text = null
//// }
//// }
////
//// // TODO: Migrate the image animation stuff from SongView into this class
////
//// if (image != null) {
//// status.setCompoundDrawablesWithIntrinsicBounds(
//// image, null, null, null
//// )
//// }
////
//// if (image === DownloadRowAdapter.downloadingImage) {
//// // FIXME
////// val frameAnimation = image as AnimationDrawable
//////
////// frameAnimation.setVisible(true, true)
////// frameAnimation.start()
//// }
//// }
// // fun updateDownloadStatus2(
// // downloadFile: DownloadFile,
// // ) {
// //
// // var image: Drawable? = null
// //
// // when (downloadFile.status.value) {
// // DownloadStatus.DONE -> {
// // image = if (downloadFile.isSaved) DownloadRowAdapter.pinImage else DownloadRowAdapter.downloadedImage
// // status.text = null
// // }
// // DownloadStatus.DOWNLOADING -> {
// // status.text = Util.formatPercentage(downloadFile.progress.value!!)
// // image = DownloadRowAdapter.downloadingImage
// // }
// // else -> {
// // status.text = null
// // }
// // }
// //
// // // TODO: Migrate the image animation stuff from SongView into this class
// //
// // if (image != null) {
// // status.setCompoundDrawablesWithIntrinsicBounds(
// // image, null, null, null
// // )
// // }
// //
// // if (image === DownloadRowAdapter.downloadingImage) {
// // // FIXME
// //// val frameAnimation = image as AnimationDrawable
// ////
// //// frameAnimation.setVisible(true, true)
// //// frameAnimation.start()
// // }
// // }
//
// override fun setChecked(newStatus: Boolean) {
// check.isChecked = newStatus
@ -313,4 +313,4 @@
// }
//
//
//}
// }

View File

@ -2,31 +2,22 @@
<LinearLayout
xmlns:a="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
a:orientation="vertical"
a:layout_width="fill_parent"
a:layout_height="fill_parent">
<TextView
a:id="@+id/playlist_empty"
a:text="@string/download.empty"
a:text="@string/playlist.empty"
a:layout_width="fill_parent"
a:layout_height="wrap_content"
a:padding="10dip"/>
<com.mobeta.android.dslv.DragSortListView
<androidx.recyclerview.widget.RecyclerView
a:id="@+id/playlist_view"
a:layout_width="fill_parent"
a:layout_height="0dip"
a:layout_weight="1"
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" />
a:fastScrollEnabled="true" />
</LinearLayout>

View File

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

View File

@ -43,7 +43,7 @@
<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_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_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>

View File

@ -42,7 +42,7 @@
<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_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_off">Fernbedienung ausgeschaltet. Musik wird auf dem Telefon wiedergegeben.</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="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.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_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>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">Voulez-vous supprimer %1$s</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.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_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>

View File

@ -53,7 +53,7 @@
<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_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_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>

View File

@ -40,7 +40,7 @@
<string name="delete_playlist">Vuoi eliminare %1$s</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.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_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>

View File

@ -56,7 +56,7 @@
<string name="delete_playlist">Wil je %1$s verwijderen?</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.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_off">Afstandsbediening uitgeschakeld; muziek wordt afgespeeld op de telefoon.</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="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.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_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>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">Você quer excluir %1$s</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.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_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>

View File

@ -42,7 +42,7 @@
<string name="delete_playlist">Você quer apagar %1$s</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.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_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>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">Вы хотите удалить %1$s</string>
<string name="download.bookmark_removed" formatted="false">Закладка удалена</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_off">Пульт управления выключен. Музыка играет на телефоне</string>
<string name="download.jukebox_offline">Пульт дистанционного управления недоступен в автономном режиме.</string>

View File

@ -53,7 +53,7 @@
<string name="delete_playlist">确定要删除 %1$s吗</string>
<string name="download.bookmark_removed" formatted="false">书签已删除。</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_off">关闭远程控制,音乐将在手机上播放</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="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.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_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>
@ -464,6 +464,10 @@
<item quantity="one">%d song unpinned</item>
<item quantity="other">%d songs unpinned</item>
</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">
<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>