music: revamp paths
Revamp paths with an entirely new abstraction that should improve testability and integration with M3U playlists.
This commit is contained in:
parent
08ca71b7b0
commit
364675b252
50 changed files with 422 additions and 277 deletions
|
@ -102,10 +102,7 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment<DialogSongDetailBindi
|
|||
}
|
||||
add(SongProperty(R.string.lbl_disc, zipped))
|
||||
}
|
||||
add(SongProperty(R.string.lbl_file_name, song.path.name))
|
||||
add(
|
||||
SongProperty(
|
||||
R.string.lbl_relative_path, song.path.parent.resolveName(context)))
|
||||
add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
|
||||
info.resolvedMimeType.resolveName(context)?.let {
|
||||
add(SongProperty(R.string.lbl_format, it))
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ import androidx.core.content.edit
|
|||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.fs.Directory
|
||||
import org.oxycblt.auxio.music.fs.MusicDirectories
|
||||
import org.oxycblt.auxio.music.dirs.DocumentTreePathFactory
|
||||
import org.oxycblt.auxio.music.dirs.MusicDirectories
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -55,8 +55,12 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
|
|||
}
|
||||
}
|
||||
|
||||
class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
|
||||
Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
|
||||
class MusicSettingsImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@ApplicationContext context: Context,
|
||||
val documentTreePathFactory: DocumentTreePathFactory
|
||||
) : Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
|
||||
private val storageManager = context.getSystemServiceCompat(StorageManager::class)
|
||||
|
||||
override var musicDirs: MusicDirectories
|
||||
|
@ -64,7 +68,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
|||
val dirs =
|
||||
(sharedPreferences.getStringSet(getString(R.string.set_key_music_dirs), null)
|
||||
?: emptySet())
|
||||
.mapNotNull { Directory.fromDocumentTreeUri(storageManager, it) }
|
||||
.mapNotNull(documentTreePathFactory::deserializeDocumentTreePath)
|
||||
return MusicDirectories(
|
||||
dirs,
|
||||
sharedPreferences.getBoolean(getString(R.string.set_key_music_dirs_include), false))
|
||||
|
@ -73,7 +77,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context
|
|||
sharedPreferences.edit {
|
||||
putStringSet(
|
||||
getString(R.string.set_key_music_dirs),
|
||||
value.dirs.map(Directory::toDocumentTreeUri).toSet())
|
||||
value.dirs.map(documentTreePathFactory::serializeDocumentTreePath).toSet())
|
||||
putBoolean(getString(R.string.set_key_music_dirs_include), value.shouldInclude)
|
||||
apply()
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ import org.oxycblt.auxio.music.Music
|
|||
import org.oxycblt.auxio.music.MusicType
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.fs.MimeType
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
import org.oxycblt.auxio.music.fs.toAudioUri
|
||||
import org.oxycblt.auxio.music.fs.toCoverUri
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
|
@ -85,14 +84,10 @@ class SongImpl(
|
|||
requireNotNull(rawSong.mediaStoreId) { "Invalid raw ${rawSong.fileName}: No id" }
|
||||
.toAudioUri()
|
||||
override val path =
|
||||
Path(
|
||||
name =
|
||||
requireNotNull(rawSong.directory) { "Invalid raw ${rawSong.fileName}: No parent directory" }
|
||||
.file(
|
||||
requireNotNull(rawSong.fileName) {
|
||||
"Invalid raw ${rawSong.fileName}: No display name"
|
||||
},
|
||||
parent =
|
||||
requireNotNull(rawSong.directory) {
|
||||
"Invalid raw ${rawSong.fileName}: No parent directory"
|
||||
})
|
||||
override val mimeType =
|
||||
MimeType(
|
||||
|
@ -247,11 +242,11 @@ class SongImpl(
|
|||
* @return This instance upcasted to [Song].
|
||||
*/
|
||||
fun finalize(): Song {
|
||||
checkNotNull(_album) { "Malformed song ${path.name}: No album" }
|
||||
checkNotNull(_album) { "Malformed song ${path}: No album" }
|
||||
|
||||
check(_artists.isNotEmpty()) { "Malformed song ${path.name}: No artists" }
|
||||
check(_artists.isNotEmpty()) { "Malformed song ${path}: No artists" }
|
||||
check(_artists.size == rawArtists.size) {
|
||||
"Malformed song ${path.name}: Artist grouping mismatch"
|
||||
"Malformed song ${path}: Artist grouping mismatch"
|
||||
}
|
||||
for (i in _artists.indices) {
|
||||
// Non-destructively reorder the linked artists so that they align with
|
||||
|
@ -262,10 +257,8 @@ class SongImpl(
|
|||
_artists[i] = other
|
||||
}
|
||||
|
||||
check(_genres.isNotEmpty()) { "Malformed song ${path.name}: No genres" }
|
||||
check(_genres.size == rawGenres.size) {
|
||||
"Malformed song ${path.name}: Genre grouping mismatch"
|
||||
}
|
||||
check(_genres.isNotEmpty()) { "Malformed song ${path}: No genres" }
|
||||
check(_genres.size == rawGenres.size) { "Malformed song ${path}: Genre grouping mismatch" }
|
||||
for (i in _genres.indices) {
|
||||
// Non-destructively reorder the linked genres so that they align with
|
||||
// the genre ordering within the song metadata.
|
||||
|
@ -519,6 +512,7 @@ class ArtistImpl(
|
|||
return this
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Library-backed implementation of [Genre].
|
||||
*
|
||||
|
|
|
@ -22,7 +22,7 @@ import java.util.UUID
|
|||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.fs.Directory
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.info.ReleaseType
|
||||
|
||||
|
@ -44,7 +44,7 @@ data class RawSong(
|
|||
/** @see Song.path */
|
||||
var fileName: String? = null,
|
||||
/** @see Song.path */
|
||||
var directory: Directory? = null,
|
||||
var directory: Path? = null,
|
||||
/** @see Song.size */
|
||||
var size: Long? = null,
|
||||
/** @see Song.durationMs */
|
||||
|
|
|
@ -16,13 +16,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.fs
|
||||
package org.oxycblt.auxio.music.dirs
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
|
||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -35,11 +36,11 @@ import org.oxycblt.auxio.util.logD
|
|||
*/
|
||||
class DirectoryAdapter(private val listener: Listener) :
|
||||
RecyclerView.Adapter<MusicDirViewHolder>() {
|
||||
private val _dirs = mutableListOf<Directory>()
|
||||
private val _dirs = mutableListOf<Path>()
|
||||
/**
|
||||
* The current list of [Directory]s, may not line up with [MusicDirectories] due to removals.
|
||||
* The current list of [SystemPath]s, may not line up with [MusicDirectories] due to removals.
|
||||
*/
|
||||
val dirs: List<Directory> = _dirs
|
||||
val dirs: List<Path> = _dirs
|
||||
|
||||
override fun getItemCount() = dirs.size
|
||||
|
||||
|
@ -50,37 +51,37 @@ class DirectoryAdapter(private val listener: Listener) :
|
|||
holder.bind(dirs[position], listener)
|
||||
|
||||
/**
|
||||
* Add a [Directory] to the end of the list.
|
||||
* Add a [Path] to the end of the list.
|
||||
*
|
||||
* @param dir The [Directory] to add.
|
||||
* @param path The [Path] to add.
|
||||
*/
|
||||
fun add(dir: Directory) {
|
||||
if (_dirs.contains(dir)) return
|
||||
logD("Adding $dir")
|
||||
_dirs.add(dir)
|
||||
fun add(path: Path) {
|
||||
if (_dirs.contains(path)) return
|
||||
logD("Adding $path")
|
||||
_dirs.add(path)
|
||||
notifyItemInserted(_dirs.lastIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a list of [Directory] instances to the end of the list.
|
||||
* Add a list of [Path] instances to the end of the list.
|
||||
*
|
||||
* @param dirs The [Directory] instances to add.
|
||||
* @param path The [Path] instances to add.
|
||||
*/
|
||||
fun addAll(dirs: List<Directory>) {
|
||||
logD("Adding ${dirs.size} directories")
|
||||
val oldLastIndex = dirs.lastIndex
|
||||
_dirs.addAll(dirs)
|
||||
notifyItemRangeInserted(oldLastIndex, dirs.size)
|
||||
fun addAll(path: List<Path>) {
|
||||
logD("Adding ${path.size} directories")
|
||||
val oldLastIndex = path.lastIndex
|
||||
_dirs.addAll(path)
|
||||
notifyItemRangeInserted(oldLastIndex, path.size)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a [Directory] from the list.
|
||||
* Remove a [Path] from the list.
|
||||
*
|
||||
* @param dir The [Directory] to remove. Must exist in the list.
|
||||
* @param path The [Path] to remove. Must exist in the list.
|
||||
*/
|
||||
fun remove(dir: Directory) {
|
||||
logD("Removing $dir")
|
||||
val idx = _dirs.indexOf(dir)
|
||||
fun remove(path: Path) {
|
||||
logD("Removing $path")
|
||||
val idx = _dirs.indexOf(path)
|
||||
_dirs.removeAt(idx)
|
||||
notifyItemRemoved(idx)
|
||||
}
|
||||
|
@ -88,7 +89,7 @@ class DirectoryAdapter(private val listener: Listener) :
|
|||
/** A Listener for [DirectoryAdapter] interactions. */
|
||||
interface Listener {
|
||||
/** Called when the delete button on a directory item is clicked. */
|
||||
fun onRemoveDirectory(dir: Directory)
|
||||
fun onRemoveDirectory(dir: Path)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,12 +103,12 @@ class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBi
|
|||
/**
|
||||
* Bind new data to this instance.
|
||||
*
|
||||
* @param dir The new [Directory] to bind.
|
||||
* @param path The new [Path] to bind.
|
||||
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(dir: Directory, listener: DirectoryAdapter.Listener) {
|
||||
binding.dirPath.text = dir.resolveName(binding.context)
|
||||
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(dir) }
|
||||
fun bind(path: Path, listener: DirectoryAdapter.Listener) {
|
||||
binding.dirPath.text = path.resolve(binding.context)
|
||||
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(path) }
|
||||
}
|
||||
|
||||
companion object {
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* DirectoryModule.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.dirs
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface DirectoryModule {
|
||||
@Binds
|
||||
fun documentTreePathFactory(factory: DocumentTreePathFactoryImpl): DocumentTreePathFactory
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* DocumentTreePathFactory.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.dirs
|
||||
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.music.fs.Components
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
import org.oxycblt.auxio.music.fs.Volume
|
||||
import org.oxycblt.auxio.music.fs.VolumeManager
|
||||
|
||||
/**
|
||||
* A factory for parsing the reverse-engineered format of the URIs obtained from the document tree
|
||||
* (i.e directory) folder.
|
||||
*
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
interface DocumentTreePathFactory {
|
||||
/**
|
||||
* Unpacks a document tree URI into a [Path] instance, using [deserializeDocumentTreePath].
|
||||
*
|
||||
* @param uri The document tree URI to unpack.
|
||||
* @return The [Path] instance, or null if the URI could not be unpacked.
|
||||
*/
|
||||
fun unpackDocumentTreeUri(uri: Uri): Path?
|
||||
|
||||
/**
|
||||
* Serializes a [Path] instance into a document tree URI format path.
|
||||
*
|
||||
* @param path The [Path] instance to serialize.
|
||||
* @return The serialized path.
|
||||
*/
|
||||
fun serializeDocumentTreePath(path: Path): String
|
||||
|
||||
/**
|
||||
* Deserializes a document tree URI format path into a [Path] instance.
|
||||
*
|
||||
* @param path The path to deserialize.
|
||||
* @return The [Path] instance, or null if the path could not be deserialized.
|
||||
*/
|
||||
fun deserializeDocumentTreePath(path: String): Path?
|
||||
}
|
||||
|
||||
class DocumentTreePathFactoryImpl @Inject constructor(private val volumeManager: VolumeManager) :
|
||||
DocumentTreePathFactory {
|
||||
override fun unpackDocumentTreeUri(uri: Uri): Path? {
|
||||
// Convert the document tree URI into it's relative path form, which can then be
|
||||
// parsed into a Directory instance.
|
||||
val docUri =
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri, DocumentsContract.getTreeDocumentId(uri))
|
||||
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
|
||||
return deserializeDocumentTreePath(treeUri)
|
||||
}
|
||||
|
||||
override fun serializeDocumentTreePath(path: Path): String =
|
||||
when (val volume = path.volume) {
|
||||
// The primary storage has a volume prefix of "primary", regardless
|
||||
// of if it's internal or not.
|
||||
is Volume.Internal -> "$DOCUMENT_URI_PRIMARY_NAME:${path.components}"
|
||||
// Document tree URIs consist of a prefixed volume name followed by a relative path.
|
||||
is Volume.External -> "${volume.id}:${path.components}"
|
||||
}
|
||||
|
||||
override fun deserializeDocumentTreePath(path: String): Path? {
|
||||
// Document tree URIs consist of a prefixed volume name followed by a relative path,
|
||||
// delimited with a colon.
|
||||
val split = path.split(File.pathSeparator, limit = 2)
|
||||
val volume =
|
||||
when (split[0]) {
|
||||
// The primary storage has a volume prefix of "primary", regardless
|
||||
// of if it's internal or not.
|
||||
DOCUMENT_URI_PRIMARY_NAME -> volumeManager.getInternalVolume()
|
||||
// Removable storage has a volume prefix of it's UUID, try to find it
|
||||
// within StorageManager's volume list.
|
||||
else ->
|
||||
volumeManager.getVolumes().find { it is Volume.External && it.id == split[0] }
|
||||
}
|
||||
val relativePath = split.getOrNull(1) ?: return null
|
||||
return Path(volume ?: return null, Components.parse(relativePath))
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DOCUMENT_URI_PRIMARY_NAME = "primary"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Auxio Project
|
||||
* MusicDirectories.kt is part of Auxio.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.dirs
|
||||
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
|
||||
/**
|
||||
* Represents the configuration for specific directories to filter to/from when loading music.
|
||||
*
|
||||
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
|
||||
* @param shouldInclude True if the library should only load from the [Directory] instances, false
|
||||
* if the library should not load from the [Directory] instances.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
data class MusicDirectories(val dirs: List<Path>, val shouldInclude: Boolean)
|
|
@ -16,13 +16,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.music.fs
|
||||
package org.oxycblt.auxio.music.dirs
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.DocumentsContract
|
||||
import android.view.LayoutInflater
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -35,8 +33,8 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
||||
import org.oxycblt.auxio.music.MusicSettings
|
||||
import org.oxycblt.auxio.music.fs.Path
|
||||
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
|
||||
|
@ -50,7 +48,7 @@ class MusicDirsDialog :
|
|||
ViewBindingMaterialDialogFragment<DialogMusicDirsBinding>(), DirectoryAdapter.Listener {
|
||||
private val dirAdapter = DirectoryAdapter(this)
|
||||
private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null
|
||||
private var storageManager: StorageManager? = null
|
||||
@Inject lateinit var documentTreePathFactory: DocumentTreePathFactory
|
||||
@Inject lateinit var musicSettings: MusicSettings
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
|
@ -70,10 +68,6 @@ class MusicDirsDialog :
|
|||
}
|
||||
|
||||
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
|
||||
val context = requireContext()
|
||||
val storageManager =
|
||||
context.getSystemServiceCompat(StorageManager::class).also { storageManager = it }
|
||||
|
||||
openDocumentTreeLauncher =
|
||||
registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
|
||||
|
@ -107,9 +101,8 @@ class MusicDirsDialog :
|
|||
if (pendingDirs != null) {
|
||||
dirs =
|
||||
MusicDirectories(
|
||||
pendingDirs.mapNotNull {
|
||||
Directory.fromDocumentTreeUri(storageManager, it)
|
||||
},
|
||||
pendingDirs.mapNotNull(
|
||||
documentTreePathFactory::deserializeDocumentTreePath),
|
||||
savedInstanceState.getBoolean(KEY_PENDING_MODE))
|
||||
}
|
||||
}
|
||||
|
@ -133,18 +126,18 @@ class MusicDirsDialog :
|
|||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putStringArrayList(
|
||||
KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() }))
|
||||
KEY_PENDING_DIRS,
|
||||
ArrayList(dirAdapter.dirs.map(documentTreePathFactory::serializeDocumentTreePath)))
|
||||
outState.putBoolean(KEY_PENDING_MODE, isUiModeInclude(requireBinding()))
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: DialogMusicDirsBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
storageManager = null
|
||||
openDocumentTreeLauncher = null
|
||||
binding.dirsRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onRemoveDirectory(dir: Directory) {
|
||||
override fun onRemoveDirectory(dir: Path) {
|
||||
dirAdapter.remove(dir)
|
||||
requireBinding().dirsEmpty.isVisible = dirAdapter.dirs.isEmpty()
|
||||
}
|
||||
|
@ -162,15 +155,7 @@ class MusicDirsDialog :
|
|||
return
|
||||
}
|
||||
|
||||
// Convert the document tree URI into it's relative path form, which can then be
|
||||
// parsed into a Directory instance.
|
||||
val docUri =
|
||||
DocumentsContract.buildDocumentUriUsingTree(
|
||||
uri, DocumentsContract.getTreeDocumentId(uri))
|
||||
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
|
||||
val dir =
|
||||
Directory.fromDocumentTreeUri(
|
||||
requireNotNull(storageManager) { "StorageManager was not available" }, treeUri)
|
||||
val dir = documentTreePathFactory.unpackDocumentTreeUri(uri)
|
||||
|
||||
if (dir != null) {
|
||||
dirAdapter.add(dir)
|
|
@ -24,119 +24,174 @@ import android.os.storage.StorageManager
|
|||
import android.os.storage.StorageVolume
|
||||
import android.webkit.MimeTypeMap
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* A full absolute path to a file. Only intended for display purposes. For accessing files, URIs are
|
||||
* preferred in all cases due to scoped storage limitations.
|
||||
* An abstraction of an android file system path, including the volume and relative path.
|
||||
*
|
||||
* @param name The name of the file.
|
||||
* @param parent The parent [Directory] of the file.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
* @param volume The volume that the path is on.
|
||||
* @param components The components of the path of the file, relative to the root of the volume.
|
||||
*/
|
||||
data class Path(val name: String, val parent: Directory)
|
||||
data class Path(
|
||||
val volume: Volume,
|
||||
val components: Components,
|
||||
) {
|
||||
/** The name of the file/directory. */
|
||||
val name: String?
|
||||
get() = components.name
|
||||
|
||||
/**
|
||||
* A volume-aware relative path to a directory.
|
||||
*
|
||||
* @param volume The [StorageVolume] that the [Directory] is contained in.
|
||||
* @param relativePath The relative path from within the [StorageVolume] to the [Directory].
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
class Directory private constructor(val volume: StorageVolume, val relativePath: String) {
|
||||
/**
|
||||
* Resolve the [Directory] instance into a human-readable path name.
|
||||
*
|
||||
* @param context [Context] required to obtain volume descriptions.
|
||||
* @return A human-readable path.
|
||||
* @see StorageVolume.getDescription
|
||||
*/
|
||||
fun resolveName(context: Context) =
|
||||
context.getString(R.string.fmt_path, volume.getDescriptionCompat(context), relativePath)
|
||||
/** The parent directory of the path, or itself if it's the root path. */
|
||||
val directory: Path
|
||||
get() = Path(volume, components.parent())
|
||||
|
||||
override fun toString() = "Path(storageVolume=$volume, components=$components)"
|
||||
|
||||
/**
|
||||
* Converts this [Directory] instance into an opaque document tree path. This is a huge
|
||||
* violation of the document tree URI contract, but it's also the only one can sensibly work
|
||||
* with these uris in the UI, and it doesn't exactly matter since we never write or read to
|
||||
* directory.
|
||||
* Transforms this [Path] into a "file" of the given name that's within the "directory"
|
||||
* represented by the current path. Ex. "/storage/emulated/0/Music" ->
|
||||
* "/storage/emulated/0/Music/file.mp3"
|
||||
*
|
||||
* @return A URI [String] abiding by the document tree specification, or null if the [Directory]
|
||||
* is not valid.
|
||||
* @param fileName The name of the file to append to the path.
|
||||
* @return The new [Path] instance.
|
||||
*/
|
||||
fun toDocumentTreeUri() =
|
||||
// Document tree URIs consist of a prefixed volume name followed by a relative path.
|
||||
if (volume.isInternalCompat) {
|
||||
// The primary storage has a volume prefix of "primary", regardless
|
||||
// of if it's internal or not.
|
||||
"$DOCUMENT_URI_PRIMARY_NAME:$relativePath"
|
||||
} else {
|
||||
// Removable storage has a volume prefix of it's UUID.
|
||||
volume.uuidCompat?.let { uuid -> "$uuid:$relativePath" }
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = volume.hashCode()
|
||||
result = 31 * result + relativePath.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is Directory && other.volume == volume && other.relativePath == relativePath
|
||||
|
||||
companion object {
|
||||
/** The name given to the internal volume when in a document tree URI. */
|
||||
private const val DOCUMENT_URI_PRIMARY_NAME = "primary"
|
||||
fun file(fileName: String) = Path(volume, components.child(fileName))
|
||||
|
||||
/**
|
||||
* Create a new directory instance from the given components.
|
||||
* Resolves the [Path] in a human-readable format.
|
||||
*
|
||||
* @param volume The [StorageVolume] that the [Directory] is contained in.
|
||||
* @param relativePath The relative path from within the [StorageVolume] to the [Directory].
|
||||
* Will be stripped of any trailing separators for a consistent internal representation.
|
||||
* @return A new [Directory] created from the components.
|
||||
* @param context [Context] required to obtain human-readable strings.
|
||||
*/
|
||||
fun from(volume: StorageVolume, relativePath: String) =
|
||||
Directory(
|
||||
volume, relativePath.removePrefix(File.separator).removeSuffix(File.separator))
|
||||
fun resolve(context: Context) = "${volume.resolveName(context)}/$components"
|
||||
}
|
||||
|
||||
sealed interface Volume {
|
||||
/** The name of the volume as it appears in MediaStore. */
|
||||
val mediaStoreName: String?
|
||||
|
||||
/**
|
||||
* Create a new directory from a document tree URI. This is a huge violation of the document
|
||||
* tree URI contract, but it's also the only one can sensibly work with these uris in the
|
||||
* UI, and it doesn't exactly matter since we never write or read directory.
|
||||
*
|
||||
* @param storageManager [StorageManager] in order to obtain the [StorageVolume] specified
|
||||
* in the given URI.
|
||||
* @param uri The URI string to parse into a [Directory].
|
||||
* @return A new [Directory] parsed from the URI, or null if the URI is not valid.
|
||||
* The components of the path to the volume, relative from the system root. Should not be used
|
||||
* except for compatibility purposes.
|
||||
*/
|
||||
fun fromDocumentTreeUri(storageManager: StorageManager, uri: String): Directory? {
|
||||
// Document tree URIs consist of a prefixed volume name followed by a relative path,
|
||||
// delimited with a colon.
|
||||
val split = uri.split(File.pathSeparator, limit = 2)
|
||||
val volume =
|
||||
when (split[0]) {
|
||||
// The primary storage has a volume prefix of "primary", regardless
|
||||
// of if it's internal or not.
|
||||
DOCUMENT_URI_PRIMARY_NAME -> storageManager.primaryStorageVolumeCompat
|
||||
// Removable storage has a volume prefix of it's UUID, try to find it
|
||||
// within StorageManager's volume list.
|
||||
else -> storageManager.storageVolumesCompat.find { it.uuidCompat == split[0] }
|
||||
}
|
||||
val relativePath = split.getOrNull(1)
|
||||
return from(volume ?: return null, relativePath ?: return null)
|
||||
}
|
||||
val components: Components?
|
||||
|
||||
/** Resolves the name of the volume in a human-readable format. */
|
||||
fun resolveName(context: Context): String
|
||||
|
||||
/** A volume representing the device's internal storage. */
|
||||
interface Internal : Volume
|
||||
|
||||
/** A volume representing an external storage device, identified by a UUID. */
|
||||
interface External : Volume {
|
||||
/** The UUID of the volume. */
|
||||
val id: String?
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the configuration for specific directories to filter to/from when loading music.
|
||||
* The components of a path. This allows the path to be manipulated without having tp handle
|
||||
* separator parsing.
|
||||
*
|
||||
* @param dirs A list of [Directory] instances. How these are interpreted depends on [shouldInclude]
|
||||
* @param shouldInclude True if the library should only load from the [Directory] instances, false
|
||||
* if the library should not load from the [Directory] instances.
|
||||
* @author Alexander Capehart (OxygenCobalt)
|
||||
* @param components The components of the path.
|
||||
*/
|
||||
data class MusicDirectories(val dirs: List<Directory>, val shouldInclude: Boolean)
|
||||
@JvmInline
|
||||
value class Components private constructor(val components: List<String>) {
|
||||
/** The name of the file/directory. */
|
||||
val name: String?
|
||||
get() = components.lastOrNull()
|
||||
|
||||
override fun toString() = components.joinToString(File.separator)
|
||||
|
||||
/**
|
||||
* Returns a new [Components] instance with the last element of the path removed as a "parent"
|
||||
* element of the original instance.
|
||||
*
|
||||
* @return The new [Components] instance, or the original instance if it's the root path.
|
||||
*/
|
||||
fun parent() = Components(components.dropLast(1))
|
||||
|
||||
/**
|
||||
* Returns a new [Components] instance with the given name appended to the end of the path as a
|
||||
* "child" element of the original instance.
|
||||
*
|
||||
* @param name The name of the file/directory to append to the path.
|
||||
*/
|
||||
fun child(name: String) =
|
||||
if (name.isNotEmpty()) {
|
||||
Components(components + name.trimSlashes()).also { logD(it.components) }
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Parses a path string into a [Components] instance by the system path separator.
|
||||
*
|
||||
* @param path The path string to parse.
|
||||
* @return The [Components] instance.
|
||||
*/
|
||||
fun parse(path: String) =
|
||||
Components(path.trimSlashes().split(File.separatorChar).filter { it.isNotEmpty() })
|
||||
|
||||
private fun String.trimSlashes() = trimStart(File.separatorChar).trimEnd(File.separatorChar)
|
||||
}
|
||||
}
|
||||
|
||||
/** A wrapper around [StorageManager] that provides instances of the [Volume] interface. */
|
||||
interface VolumeManager {
|
||||
/**
|
||||
* The internal storage volume of the device.
|
||||
*
|
||||
* @see StorageManager.getPrimaryStorageVolume
|
||||
*/
|
||||
fun getInternalVolume(): Volume.Internal
|
||||
|
||||
/**
|
||||
* The list of [Volume]s currently recognized by [StorageManager].
|
||||
*
|
||||
* @see StorageManager.getStorageVolumes
|
||||
*/
|
||||
fun getVolumes(): List<Volume>
|
||||
}
|
||||
|
||||
class VolumeManagerImpl @Inject constructor(private val storageManager: StorageManager) :
|
||||
VolumeManager {
|
||||
override fun getInternalVolume(): Volume.Internal =
|
||||
InternalVolumeImpl(storageManager.primaryStorageVolume)
|
||||
|
||||
override fun getVolumes() =
|
||||
storageManager.storageVolumesCompat.map {
|
||||
if (it.isInternalCompat) {
|
||||
InternalVolumeImpl(it)
|
||||
} else {
|
||||
ExternalVolumeImpl(it)
|
||||
}
|
||||
}
|
||||
|
||||
private class InternalVolumeImpl(val storageVolume: StorageVolume) : Volume.Internal {
|
||||
override val mediaStoreName
|
||||
get() = storageVolume.mediaStoreVolumeNameCompat
|
||||
|
||||
override val components
|
||||
get() = storageVolume.directoryCompat?.let(Components::parse)
|
||||
|
||||
override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
|
||||
}
|
||||
|
||||
private class ExternalVolumeImpl(val storageVolume: StorageVolume) : Volume.External {
|
||||
override val id
|
||||
get() = storageVolume.uuidCompat
|
||||
|
||||
override val mediaStoreName
|
||||
get() = storageVolume.mediaStoreVolumeNameCompat
|
||||
|
||||
override val components
|
||||
get() = storageVolume.directoryCompat?.let(Components::parse)
|
||||
|
||||
override fun resolveName(context: Context) = storageVolume.getDescriptionCompat(context)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A mime type of a file. Only intended for display.
|
||||
|
|
|
@ -19,16 +19,22 @@
|
|||
package org.oxycblt.auxio.music.fs
|
||||
|
||||
import android.content.Context
|
||||
import android.os.storage.StorageManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class FsModule {
|
||||
@Provides
|
||||
fun mediaStoreExtractor(@ApplicationContext context: Context) =
|
||||
MediaStoreExtractor.from(context)
|
||||
fun volumeManager(@ApplicationContext context: Context): VolumeManager =
|
||||
VolumeManagerImpl(context.getSystemServiceCompat(StorageManager::class))
|
||||
|
||||
@Provides
|
||||
fun mediaStoreExtractor(@ApplicationContext context: Context, volumeManager: VolumeManager) =
|
||||
MediaStoreExtractor.from(context, volumeManager)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ package org.oxycblt.auxio.music.fs
|
|||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.storage.StorageManager
|
||||
import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.database.getIntOrNull
|
||||
|
@ -31,10 +30,10 @@ import kotlinx.coroutines.channels.Channel
|
|||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.music.cache.Cache
|
||||
import org.oxycblt.auxio.music.device.RawSong
|
||||
import org.oxycblt.auxio.music.dirs.MusicDirectories
|
||||
import org.oxycblt.auxio.music.info.Date
|
||||
import org.oxycblt.auxio.music.metadata.parseId3v2PositionField
|
||||
import org.oxycblt.auxio.music.metadata.transformPositionField
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.sendWithTimeout
|
||||
|
||||
|
@ -93,13 +92,16 @@ interface MediaStoreExtractor {
|
|||
* Create a framework-backed instance.
|
||||
*
|
||||
* @param context [Context] required.
|
||||
* @param volumeManager [VolumeManager] required.
|
||||
* @return A new [MediaStoreExtractor] that will work best on the device's API level.
|
||||
*/
|
||||
fun from(context: Context): MediaStoreExtractor =
|
||||
fun from(context: Context, volumeManager: VolumeManager): MediaStoreExtractor =
|
||||
when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreExtractor(context)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreExtractor(context)
|
||||
else -> Api21MediaStoreExtractor(context)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
|
||||
Api30MediaStoreExtractor(context, volumeManager)
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ->
|
||||
Api29MediaStoreExtractor(context, volumeManager)
|
||||
else -> Api21MediaStoreExtractor(context, volumeManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -249,15 +251,15 @@ private abstract class BaseMediaStoreExtractor(protected val context: Context) :
|
|||
protected abstract val dirSelectorTemplate: String
|
||||
|
||||
/**
|
||||
* Add a [Directory] to the given list of projection selector arguments.
|
||||
* Add a [SystemPath] to the given list of projection selector arguments.
|
||||
*
|
||||
* @param dir The [Directory] to add.
|
||||
* @param path The [SystemPath] to add.
|
||||
* @param args The destination list to append selector arguments to that are analogous to the
|
||||
* given [Directory].
|
||||
* @return true if the [Directory] was added, false otherwise.
|
||||
* given [SystemPath].
|
||||
* @return true if the [SystemPath] was added, false otherwise.
|
||||
* @see dirSelectorTemplate
|
||||
*/
|
||||
protected abstract fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean
|
||||
protected abstract fun addDirToSelector(path: Path, args: MutableList<String>): Boolean
|
||||
|
||||
protected abstract fun wrapQuery(
|
||||
cursor: Cursor,
|
||||
|
@ -362,7 +364,8 @@ private abstract class BaseMediaStoreExtractor(protected val context: Context) :
|
|||
// Note: The separation between version-specific backends may not be the cleanest. To preserve
|
||||
// speed, we only want to add redundancy on known issues, not with possible issues.
|
||||
|
||||
private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtractor(context) {
|
||||
private class Api21MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) :
|
||||
BaseMediaStoreExtractor(context) {
|
||||
override val projection: Array<String>
|
||||
get() =
|
||||
super.projection +
|
||||
|
@ -378,28 +381,27 @@ private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtract
|
|||
override val dirSelectorTemplate: String
|
||||
get() = "${MediaStore.Audio.Media.DATA} LIKE ?"
|
||||
|
||||
override fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean {
|
||||
override fun addDirToSelector(path: Path, args: MutableList<String>): Boolean {
|
||||
// "%" signifies to accept any DATA value that begins with the Directory's path,
|
||||
// thus recursively filtering all files in the directory.
|
||||
args.add("${dir.volume.directoryCompat ?: return false}/${dir.relativePath}%")
|
||||
args.add("${path.volume.components ?: return false}${path.components}%")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun wrapQuery(
|
||||
cursor: Cursor,
|
||||
genreNamesMap: Map<Long, String>,
|
||||
): MediaStoreExtractor.Query =
|
||||
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
|
||||
): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager)
|
||||
|
||||
private class Query(
|
||||
cursor: Cursor,
|
||||
genreNamesMap: Map<Long, String>,
|
||||
storageManager: StorageManager
|
||||
volumeManager: VolumeManager
|
||||
) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
|
||||
// Set up cursor indices for later use.
|
||||
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||
private val dataIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.DATA)
|
||||
private val volumes = storageManager.storageVolumesCompat
|
||||
private val volumes = volumeManager.getVolumes()
|
||||
|
||||
override fun populateFileInfo(rawSong: RawSong) {
|
||||
super.populateFileInfo(rawSong)
|
||||
|
@ -417,10 +419,10 @@ private class Api21MediaStoreExtractor(context: Context) : BaseMediaStoreExtract
|
|||
// the Directory we will use.
|
||||
val rawPath = data.substringBeforeLast(File.separatorChar)
|
||||
for (volume in volumes) {
|
||||
val volumePath = volume.directoryCompat ?: continue
|
||||
val volumePath = (volume.components ?: continue).toString()
|
||||
val strippedPath = rawPath.removePrefix(volumePath)
|
||||
if (strippedPath != rawPath) {
|
||||
rawSong.directory = Directory.from(volume, strippedPath)
|
||||
rawSong.directory = Path(volume, Components.parse(strippedPath))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -466,26 +468,26 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) :
|
|||
"(${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " +
|
||||
"AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)"
|
||||
|
||||
override fun addDirToSelector(dir: Directory, args: MutableList<String>): Boolean {
|
||||
override fun addDirToSelector(path: Path, args: MutableList<String>): Boolean {
|
||||
// MediaStore uses a different naming scheme for it's volume column convert this
|
||||
// directory's volume to it.
|
||||
args.add(dir.volume.mediaStoreVolumeNameCompat ?: return false)
|
||||
args.add(path.volume.mediaStoreName ?: return false)
|
||||
// "%" signifies to accept any DATA value that begins with the Directory's path,
|
||||
// thus recursively filtering all files in the directory.
|
||||
args.add("${dir.relativePath}%")
|
||||
args.add("${path.components}%")
|
||||
return true
|
||||
}
|
||||
|
||||
abstract class Query(
|
||||
cursor: Cursor,
|
||||
genreNamesMap: Map<Long, String>,
|
||||
storageManager: StorageManager
|
||||
private val volumeManager: VolumeManager
|
||||
) : BaseMediaStoreExtractor.Query(cursor, genreNamesMap) {
|
||||
private val volumeIndex =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.VOLUME_NAME)
|
||||
private val relativePathIndex =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.RELATIVE_PATH)
|
||||
private val volumes = storageManager.storageVolumesCompat
|
||||
private val volumes = volumeManager.getVolumes()
|
||||
|
||||
final override fun populateFileInfo(rawSong: RawSong) {
|
||||
super.populateFileInfo(rawSong)
|
||||
|
@ -493,9 +495,9 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) :
|
|||
// This is combined with the plain relative path column to create the directory.
|
||||
val volumeName = cursor.getString(volumeIndex)
|
||||
val relativePath = cursor.getString(relativePathIndex)
|
||||
val volume = volumes.find { it.mediaStoreVolumeNameCompat == volumeName }
|
||||
val volume = volumes.find { it.mediaStoreName == volumeName }
|
||||
if (volume != null) {
|
||||
rawSong.directory = Directory.from(volume, relativePath)
|
||||
rawSong.directory = Path(volume, Components.parse(relativePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -509,7 +511,8 @@ private abstract class BaseApi29MediaStoreExtractor(context: Context) :
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) {
|
||||
private class Api29MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) :
|
||||
BaseApi29MediaStoreExtractor(context) {
|
||||
|
||||
override val projection: Array<String>
|
||||
get() = super.projection + arrayOf(MediaStore.Audio.AudioColumns.TRACK)
|
||||
|
@ -517,14 +520,13 @@ private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
|
|||
override fun wrapQuery(
|
||||
cursor: Cursor,
|
||||
genreNamesMap: Map<Long, String>
|
||||
): MediaStoreExtractor.Query =
|
||||
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
|
||||
): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager)
|
||||
|
||||
private class Query(
|
||||
cursor: Cursor,
|
||||
genreNamesMap: Map<Long, String>,
|
||||
storageManager: StorageManager
|
||||
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) {
|
||||
volumeManager: VolumeManager
|
||||
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, volumeManager) {
|
||||
private val trackIndex = cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.TRACK)
|
||||
|
||||
override fun populateTags(rawSong: RawSong) {
|
||||
|
@ -549,7 +551,8 @@ private class Api29MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
|
|||
* @author Alexander Capehart (OxygenCobalt)
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreExtractor(context) {
|
||||
private class Api30MediaStoreExtractor(context: Context, private val volumeManager: VolumeManager) :
|
||||
BaseApi29MediaStoreExtractor(context) {
|
||||
override val projection: Array<String>
|
||||
get() =
|
||||
super.projection +
|
||||
|
@ -562,14 +565,13 @@ private class Api30MediaStoreExtractor(context: Context) : BaseApi29MediaStoreEx
|
|||
override fun wrapQuery(
|
||||
cursor: Cursor,
|
||||
genreNamesMap: Map<Long, String>
|
||||
): MediaStoreExtractor.Query =
|
||||
Query(cursor, genreNamesMap, context.getSystemServiceCompat(StorageManager::class))
|
||||
): MediaStoreExtractor.Query = Query(cursor, genreNamesMap, volumeManager)
|
||||
|
||||
private class Query(
|
||||
cursor: Cursor,
|
||||
genreNamesMap: Map<Long, String>,
|
||||
storageManager: StorageManager
|
||||
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, storageManager) {
|
||||
volumeManager: VolumeManager
|
||||
) : BaseApi29MediaStoreExtractor.Query(cursor, genreNamesMap, volumeManager) {
|
||||
private val trackIndex =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.AudioColumns.CD_TRACK_NUMBER)
|
||||
private val discIndex =
|
||||
|
|
|
@ -71,7 +71,7 @@ class SearchEngineImpl @Inject constructor(@ApplicationContext private val conte
|
|||
return SearchEngine.Items(
|
||||
songs =
|
||||
items.songs?.searchListImpl(query) { q, song ->
|
||||
song.path.name.contains(q, ignoreCase = true)
|
||||
song.path.name?.contains(q, ignoreCase = true) == true
|
||||
},
|
||||
albums = items.albums?.searchListImpl(query),
|
||||
artists = items.artists?.searchListImpl(query),
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
android:paddingEnd="@dimen/spacing_medium"
|
||||
android:paddingTop="@dimen/spacing_small"
|
||||
android:paddingBottom="@dimen/spacing_small"
|
||||
tools:hint="@string/lbl_file_name"
|
||||
tools:hint="@string/lbl_path"
|
||||
app:expandedHintEnabled="false">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
tools:layout="@layout/dialog_pre_amp" />
|
||||
<dialog
|
||||
android:id="@+id/music_dirs_dialog"
|
||||
android:name="org.oxycblt.auxio.music.fs.MusicDirsDialog"
|
||||
android:name="org.oxycblt.auxio.music.dirs.MusicDirsDialog"
|
||||
android:label="music_dirs_dialog"
|
||||
tools:layout="@layout/dialog_music_dirs" />
|
||||
<dialog
|
||||
|
|
|
@ -143,10 +143,8 @@
|
|||
<string name="lbl_cancel">الغاء</string>
|
||||
<string name="lbl_format">التنسيق</string>
|
||||
<string name="lbl_size">الحجم</string>
|
||||
<string name="lbl_relative_path">المسار</string>
|
||||
<string name="lbl_library_counts">إحصائيات المكتبة</string>
|
||||
<string name="lbl_bitrate">معدل البت</string>
|
||||
<string name="lbl_file_name">اسم الملف</string>
|
||||
<string name="lbl_compilation_live">تجميع مباشر</string>
|
||||
<string name="lbl_compilation_remix">تجميعات</string>
|
||||
<string name="lbl_props">خصائص الاغنية</string>
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
<string name="lbl_artist_details">اذهب للفنان</string>
|
||||
<string name="lng_widget">عرض والتحكم في تشغيل الموسيقى</string>
|
||||
<string name="lbl_shuffle_shortcut_short">خلط</string>
|
||||
<string name="lbl_file_name">اسم الملف</string>
|
||||
<string name="lbl_shuffle_shortcut_long">خلط الكل</string>
|
||||
<string name="lbl_cancel">إلغاء</string>
|
||||
<string name="lbl_save">حفظ</string>
|
||||
|
|
|
@ -83,10 +83,8 @@
|
|||
<string name="lbl_queue">Чарга</string>
|
||||
<string name="lbl_album_details">Перайсці да альбома</string>
|
||||
<string name="lbl_artist_details">Перайсці да выканаўцы</string>
|
||||
<string name="lbl_file_name">Імя файла</string>
|
||||
<string name="lbl_song_detail">Праглядзіце ўласцівасці</string>
|
||||
<string name="lbl_props">Уласцівасці песні</string>
|
||||
<string name="lbl_relative_path">Бацькоўскі шлях</string>
|
||||
<string name="lbl_format">Фармат</string>
|
||||
<string name="lbl_shuffle_shortcut_long">Перамяшаць усё</string>
|
||||
<string name="lbl_bitrate">Бітрэйт</string>
|
||||
|
|
|
@ -185,8 +185,6 @@
|
|||
<string name="set_play_song_none">Přehrát ze zobrazené položky</string>
|
||||
<string name="lbl_song_detail">Zobrazit vlastnosti</string>
|
||||
<string name="lbl_props">Vlastnosti skladby</string>
|
||||
<string name="lbl_file_name">Název souboru</string>
|
||||
<string name="lbl_relative_path">Nadřazená cesta</string>
|
||||
<string name="lbl_format">Formát</string>
|
||||
<string name="lbl_size">Velikost</string>
|
||||
<string name="lbl_bitrate">Přenosová rychlost</string>
|
||||
|
|
|
@ -179,8 +179,6 @@
|
|||
<string name="lbl_sample_rate">Abtastrate</string>
|
||||
<string name="lbl_song_detail">Eigenschaften ansehen</string>
|
||||
<string name="lbl_props">Lied-Eigenschaften</string>
|
||||
<string name="lbl_file_name">Dateiname</string>
|
||||
<string name="lbl_relative_path">Elternpfad</string>
|
||||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_size">Größe</string>
|
||||
<string name="lbl_bitrate">Bitrate</string>
|
||||
|
|
|
@ -80,7 +80,6 @@
|
|||
<string name="fmt_bitrate">%d kbps</string>
|
||||
<string name="lbl_add">Πρόσθεση</string>
|
||||
<string name="lbl_props">Ιδιότητες τραγουδιού</string>
|
||||
<string name="lbl_file_name">Όνομα αρχείου</string>
|
||||
<string name="lbl_song_detail">Προβολή Ιδιοτήτων</string>
|
||||
<string name="lbl_library_counts">Στατιστικά συλλογής</string>
|
||||
<string name="lbl_album_live">Ζωντανό άλμπουμ</string>
|
||||
|
|
|
@ -194,7 +194,6 @@
|
|||
<string name="lbl_mixtapes">Mixtapes (recopilación de canciones)</string>
|
||||
<string name="lbl_mixtape">Mixtape (recopilación de canciones)</string>
|
||||
<string name="lbl_remix_group">Remezclas</string>
|
||||
<string name="lbl_file_name">Nombre de archivo</string>
|
||||
<string name="set_headset_autoplay_desc">Siempre empezar la reproducción cuando se conecten auriculares (puede no funcionar en todos los dispositivos)</string>
|
||||
<string name="set_pre_amp">Pre-amp ReplayGain</string>
|
||||
<string name="set_pre_amp_desc">El pre-amp se aplica al ajuste existente durante la reproducción</string>
|
||||
|
@ -215,7 +214,6 @@
|
|||
<string name="lbl_single_remix">Single remix</string>
|
||||
<string name="lbl_compilations">Compilaciones</string>
|
||||
<string name="lbl_ep_remix">EP de remixes</string>
|
||||
<string name="lbl_relative_path">Directorio superior</string>
|
||||
<string name="set_wipe_desc">Eliminar el estado de reproducción guardado previamente (si existe)</string>
|
||||
<string name="desc_queue_bar">Abrir la cola</string>
|
||||
<string name="lbl_genre">Género</string>
|
||||
|
|
|
@ -43,8 +43,6 @@
|
|||
<string name="lbl_album_details">Siirry albumiin</string>
|
||||
<string name="lbl_song_detail">Näytä ominaisuudet</string>
|
||||
<string name="lbl_props">Kappaleen ominaisuudet</string>
|
||||
<string name="lbl_file_name">Tiedostonimi</string>
|
||||
<string name="lbl_relative_path">Ylätason polku</string>
|
||||
<string name="lbl_format">Muoto</string>
|
||||
<string name="lbl_size">Koko</string>
|
||||
<string name="lbl_bitrate">Bittitaajuus</string>
|
||||
|
|
|
@ -46,7 +46,6 @@
|
|||
<string name="lbl_album_details">Puntahan ang album</string>
|
||||
<string name="lbl_song_detail">Tignan ang katangian</string>
|
||||
<string name="lbl_props">Katangian ng kanta</string>
|
||||
<string name="lbl_file_name">Pangalan ng file</string>
|
||||
<string name="lbl_format">Pormat</string>
|
||||
<string name="lbl_size">Laki</string>
|
||||
<string name="lbl_bitrate">Tulin ng mga bit</string>
|
||||
|
|
|
@ -82,8 +82,6 @@
|
|||
<string name="lbl_bitrate">Débit binaire</string>
|
||||
<string name="set_notif_action">Utiliser une autre action de notification</string>
|
||||
<string name="lbl_sample_rate">Taux d\'échantillonnage</string>
|
||||
<string name="lbl_relative_path">Chemin parent</string>
|
||||
<string name="lbl_file_name">Nom du fichier</string>
|
||||
<string name="lbl_shuffle_shortcut_long">Tout mélanger</string>
|
||||
<string name="lbl_cancel">Annuler</string>
|
||||
<string name="lbl_save">Enregistrer</string>
|
||||
|
|
|
@ -53,11 +53,9 @@
|
|||
<string name="lbl_queue_add">Engadir á cola</string>
|
||||
<string name="set_exclude_non_music">Excluir o que non é música</string>
|
||||
<string name="lbl_artist_details">Ir ao artista</string>
|
||||
<string name="lbl_file_name">Nome do arquivo</string>
|
||||
<string name="lbl_shuffle_shortcut_short">Mesturar</string>
|
||||
<string name="lbl_shuffle_shortcut_long">Mesturar todo</string>
|
||||
<string name="lbl_reset">Restablecer</string>
|
||||
<string name="lbl_relative_path">Directorio superior</string>
|
||||
<string name="lbl_format">Formato</string>
|
||||
<string name="lbl_size">Tamaño</string>
|
||||
<string name="lbl_bitrate">Tasa de bits</string>
|
||||
|
|
|
@ -61,14 +61,12 @@
|
|||
<string name="info_app_desc">एंड्रॉयड के लिए एक सीधा साधा, विवेकशील गाने बजाने वाला ऐप।</string>
|
||||
<string name="lbl_new_playlist">नई प्लेलिस्ट</string>
|
||||
<string name="lbl_play_next">अगला चलाएं</string>
|
||||
<string name="lbl_file_name">फ़ाइल का नाम</string>
|
||||
<string name="set_lib_tabs">लायब्रेरी टैब्स</string>
|
||||
<string name="set_play_song_from_album">एल्बम से चलाएं</string>
|
||||
<string name="set_content">सामग्री</string>
|
||||
<string name="fmt_selected">%d चयनित</string>
|
||||
<string name="lbl_format">प्रारूप</string>
|
||||
<string name="lbl_playlist_add">प्लेलिस्ट में जोड़ें</string>
|
||||
<string name="lbl_relative_path">मुख्य पथ</string>
|
||||
<string name="lbl_bitrate">बिट-रेट</string>
|
||||
<string name="lbl_cancel">रद्द करें</string>
|
||||
<string name="lbl_save">सहेजें</string>
|
||||
|
|
|
@ -40,8 +40,6 @@
|
|||
<string name="lbl_queue">Popis pjesama</string>
|
||||
<string name="lbl_play_next">Reproduciraj sljedeću</string>
|
||||
<string name="lbl_props">Svojstva pjesme</string>
|
||||
<string name="lbl_file_name">Naziv datoteke</string>
|
||||
<string name="lbl_relative_path">Glavni direktorij</string>
|
||||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_size">Veličina</string>
|
||||
<string name="lbl_bitrate">Brzina prijenosa</string>
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
<string name="desc_shuffle">Keverés be/ki kapcsolása</string>
|
||||
<string name="desc_album_cover">%s album borítója</string>
|
||||
<string name="set_replay_gain">Visszajátszás</string>
|
||||
<string name="lbl_relative_path">Szülő útvonal</string>
|
||||
<string name="desc_music_dir_delete">Mappa eltávolítása</string>
|
||||
<string name="lbl_playlist_add">Lejátszólistához ad</string>
|
||||
<string name="lbl_format">Formátum</string>
|
||||
|
@ -160,7 +159,6 @@
|
|||
<string name="set_replay_gain_mode_dynamic">Inkább album, ha egyet játszik</string>
|
||||
<string name="set_reindex">Zene frissítése</string>
|
||||
<string name="lbl_cancel">Mégse</string>
|
||||
<string name="lbl_file_name">Fájl név</string>
|
||||
<string name="set_bar_action">Egyéni lejátszási sáv művelet</string>
|
||||
<string name="set_headset_autoplay_desc">A lejátszás mindig akkor indul el, ha a fejhallgató csatlakoztatva van (nem minden eszközön működik)</string>
|
||||
<string name="set_observing">Automatikus újratöltés</string>
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
<item quantity="other">%d Album</item>
|
||||
</plurals>
|
||||
<string name="lbl_props">Properti lagu</string>
|
||||
<string name="lbl_file_name">Nama berkas</string>
|
||||
<string name="lbl_bitrate">Laju bit</string>
|
||||
<string name="lbl_ok">OK</string>
|
||||
<string name="lbl_cancel">Batal</string>
|
||||
|
@ -65,7 +64,6 @@
|
|||
<string name="set_headset_autoplay">Putar otomatis headset</string>
|
||||
<string name="set_headset_autoplay_desc">Selalu mulai memutar ketika headset tersambung (mungkin tidak berfungsi pada semua perangkat)</string>
|
||||
<string name="set_replay_gain_mode">Strategi ReplayGain</string>
|
||||
<string name="lbl_relative_path">Path induk</string>
|
||||
<string name="lbl_size">Ukuran</string>
|
||||
<string name="lbl_sample_rate">Tingkat sampel</string>
|
||||
<string name="set_lib_tabs">Tab Pustaka</string>
|
||||
|
|
|
@ -181,8 +181,6 @@
|
|||
<string name="lbl_sample_rate">Frequenza di campionamento</string>
|
||||
<string name="lbl_song_detail">Vedi proprietà</string>
|
||||
<string name="lbl_props">Proprietà brano</string>
|
||||
<string name="lbl_file_name">Nome file</string>
|
||||
<string name="lbl_relative_path">Directory superiore</string>
|
||||
<string name="lbl_format">Formato</string>
|
||||
<string name="lbl_size">Dimensione</string>
|
||||
<string name="lbl_bitrate">Bitrate</string>
|
||||
|
|
|
@ -131,7 +131,6 @@
|
|||
<string name="lbl_track">רצועה</string>
|
||||
<string name="lbl_queue">תור</string>
|
||||
<string name="lbl_artist_details">מעבר לאומן</string>
|
||||
<string name="lbl_file_name">שם קובץ</string>
|
||||
<string name="lbl_shuffle_shortcut_short">ערבוב</string>
|
||||
<string name="lbl_state_restored">המצב שוחזר</string>
|
||||
<string name="lbl_about">אודות</string>
|
||||
|
@ -242,7 +241,6 @@
|
|||
<string name="desc_playlist_image">תמונת רשימת השמעה עבור %s</string>
|
||||
<string name="clr_red">אדום</string>
|
||||
<string name="clr_green">ירוק</string>
|
||||
<string name="lbl_relative_path">נתיב ראשי</string>
|
||||
<string name="err_did_not_restore">לא ניתן לשחזר את המצב</string>
|
||||
<string name="desc_track_number">רצועה %d</string>
|
||||
<string name="desc_new_playlist">יצירת רשימת השמעה חדשה</string>
|
||||
|
|
|
@ -57,7 +57,6 @@
|
|||
<string name="set_play_song_none">表示されたアイテムから再生</string>
|
||||
<string name="desc_exit">再生停止</string>
|
||||
<string name="clr_red">赤</string>
|
||||
<string name="lbl_file_name">ファイル名</string>
|
||||
<string name="lbl_date_added">追加した日付け</string>
|
||||
<string name="lbl_sample_rate">サンプルレート</string>
|
||||
<string name="lbl_sort_dsc">降順</string>
|
||||
|
@ -245,7 +244,6 @@
|
|||
<string name="set_cover_mode_media_store">初期 (高速読み込み)</string>
|
||||
<string name="set_replay_gain_mode_dynamic">再生中の場合はアルバムを優先</string>
|
||||
<string name="set_separators">複数値セパレータ</string>
|
||||
<string name="lbl_relative_path">親パス</string>
|
||||
<string name="set_observing_desc">変更されるたびに音楽ライブラリをリロードします (永続的な通知が必要です)</string>
|
||||
<string name="set_audio_desc">サウンドと再生の動作を構成する</string>
|
||||
<string name="set_rewind_prev">戻る前に巻き戻す</string>
|
||||
|
|
|
@ -163,7 +163,6 @@
|
|||
<string name="set_dirs_mode_include_desc"><b>추가한 폴더에서만</b> 음악을 불러옵니다.</string>
|
||||
<string name="lbl_props">곡 속성</string>
|
||||
<string name="lbl_song_detail">속성 보기</string>
|
||||
<string name="lbl_file_name">파일 이름</string>
|
||||
<string name="lbl_sample_rate">샘플 속도</string>
|
||||
<string name="lbl_bitrate">전송 속도</string>
|
||||
<string name="lbl_size">크기</string>
|
||||
|
@ -195,7 +194,6 @@
|
|||
<string name="set_separators_and">앰퍼샌드 (&)</string>
|
||||
<string name="cdc_mp3">MPEG-1 오디오</string>
|
||||
<string name="lbl_date_added">추가한 날짜</string>
|
||||
<string name="lbl_relative_path">상위 경로</string>
|
||||
<string name="set_bar_action">맞춤형 재생 동작 버튼</string>
|
||||
<string name="set_action_mode_repeat">반복 방식</string>
|
||||
<string name="desc_queue_bar">대기열 열기</string>
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
<string name="lbl_shuffle">Maišyti</string>
|
||||
<string name="lng_queue_added">Pridėtas į eilę</string>
|
||||
<string name="lbl_props">Dainų ypatybės</string>
|
||||
<string name="lbl_file_name">Failo pavadinimas</string>
|
||||
<string name="lbl_save">Išsaugoti</string>
|
||||
<string name="lbl_about">Apie</string>
|
||||
<string name="lbl_add">Pridėti</string>
|
||||
|
@ -214,7 +213,6 @@
|
|||
<string name="lbl_mix">DJ miksas</string>
|
||||
<string name="lbl_compilation_live">Gyvai kompiliacija</string>
|
||||
<string name="lbl_compilation_remix">Remikso kompiliacija</string>
|
||||
<string name="lbl_relative_path">Pirminis kelias</string>
|
||||
<string name="set_wipe_desc">Išvalyti anksčiau išsaugotą grojimo būseną (jei yra)</string>
|
||||
<string name="set_separators">Daugiareikšmiai separatoriai</string>
|
||||
<string name="set_separators_slash">Pasvirasis brūkšnys (/)</string>
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
<string name="lbl_size">വലിപ്പം</string>
|
||||
<string name="lbl_add">ചേർക്കുക</string>
|
||||
<string name="lbl_ok">ശരി</string>
|
||||
<string name="lbl_relative_path">ഉത്ഭവ പാത</string>
|
||||
<string name="lbl_cancel">റദ്ദാക്കുക</string>
|
||||
<string name="set_theme_day">വെളിച്ചം</string>
|
||||
<string name="lbl_about">കുറിച്ച്</string>
|
||||
|
|
|
@ -192,8 +192,6 @@
|
|||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_song_detail">Vis egenskaper</string>
|
||||
<string name="lbl_props">Spor-egenskaper</string>
|
||||
<string name="lbl_file_name">Filnavn</string>
|
||||
<string name="lbl_relative_path">Overnevnt sti</string>
|
||||
<string name="set_repeat_pause">Pause ved gjentagelse</string>
|
||||
<string name="clr_red">Rød</string>
|
||||
<plurals name="fmt_album_count">
|
||||
|
|
|
@ -121,9 +121,7 @@
|
|||
<string name="lbl_cancel">Annuleren</string>
|
||||
<string name="set_lib_tabs">Bibliotheek tabbladen</string>
|
||||
<string name="lbl_date">Jaar</string>
|
||||
<string name="lbl_relative_path">Ouderpad</string>
|
||||
<string name="lbl_props">Lied eigenschappen</string>
|
||||
<string name="lbl_file_name">Bestandsnaam</string>
|
||||
<string name="set_replay_gain_mode_dynamic">Voorkeur album als er een speelt</string>
|
||||
<string name="set_replay_gain_mode_track">Voorkeur titel</string>
|
||||
<string name="set_replay_gain_mode_album">Voorkeur album</string>
|
||||
|
|
|
@ -55,7 +55,6 @@
|
|||
<string name="lbl_album_details">ਐਲਬਮ \'ਤੇ ਜਾਓ</string>
|
||||
<string name="lbl_song_detail">ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ ਵੇਖੋ</string>
|
||||
<string name="lbl_props">ਗੀਤ ਦੀਆਂ ਵਿਸ਼ੇਸ਼ਤਾਵਾਂ</string>
|
||||
<string name="lbl_relative_path">ਪੇਰੈਂਟ ਮਾਰਗ</string>
|
||||
<string name="lbl_format">ਫਾਰਮੈਟ</string>
|
||||
<string name="lbl_size">ਆਕਾਰ</string>
|
||||
<string name="lbl_shuffle_shortcut_short">ਸ਼ਫਲ</string>
|
||||
|
@ -76,7 +75,6 @@
|
|||
<string name="lbl_song_count">ਗੀਤ ਦੀ ਗਿਣਤੀ</string>
|
||||
<string name="lbl_sort_dsc">ਘਟਦੇ ਹੋਏ</string>
|
||||
<string name="lbl_artist_details">ਕਲਾਕਾਰ \'ਤੇ ਜਾਓ</string>
|
||||
<string name="lbl_file_name">ਫਾਈਲ ਦਾ ਨਾਮ</string>
|
||||
<string name="lbl_bitrate">ਬਿੱਟ ਰੇਟ</string>
|
||||
<string name="lbl_sample_rate">ਸੈਂਪਲ ਰੇਟ</string>
|
||||
<string name="lbl_add">ਸ਼ਾਮਿਲ ਕਰੋ</string>
|
||||
|
|
|
@ -90,8 +90,6 @@
|
|||
<string name="set_headset_autoplay">Autoodtwarzanie w słuchawkach</string>
|
||||
<string name="clr_teal">Morski</string>
|
||||
<string name="fmt_db_pos">+%.1f dB</string>
|
||||
<string name="lbl_file_name">Nazwa pliku</string>
|
||||
<string name="lbl_relative_path">Ścieżka katalogu</string>
|
||||
<string name="lbl_format">Format</string>
|
||||
<string name="clr_cyan">Niebieskozielony</string>
|
||||
<string name="fmt_disc_no">Płyta %d</string>
|
||||
|
|
|
@ -106,7 +106,6 @@
|
|||
<string name="lbl_artist">Artista</string>
|
||||
<string name="lbl_album">Álbum</string>
|
||||
<string name="lbl_props">Propriedades da música</string>
|
||||
<string name="lbl_file_name">Nome do arquivo</string>
|
||||
<string name="lbl_format">Formato</string>
|
||||
<string name="lbl_size">Tamanho</string>
|
||||
<string name="lbl_bitrate">Taxa de bits</string>
|
||||
|
@ -158,7 +157,6 @@
|
|||
<string name="set_pre_amp_without">Ajuste em faixas sem metadados</string>
|
||||
<string name="set_headset_autoplay">Reprodução automática em fones de ouvido</string>
|
||||
<string name="set_pre_amp">Pré-amplificação da normalização de volume</string>
|
||||
<string name="lbl_relative_path">Caminho principal</string>
|
||||
<string name="lbl_ok">OK</string>
|
||||
<string name="set_display">Exibição</string>
|
||||
<string name="set_round_mode_desc">Ativa cantos arredondados em elementos adicionais da interface do usuário</string>
|
||||
|
|
|
@ -115,7 +115,6 @@
|
|||
<string name="lbl_sample_rate">Taxa de amostragem</string>
|
||||
<string name="lbl_save">Salvar</string>
|
||||
<string name="set_separators">Separadores multi-valor</string>
|
||||
<string name="lbl_file_name">Nome do ficheiro</string>
|
||||
<string name="lbl_size">Tamanho</string>
|
||||
<string name="lbl_song_detail">Propriedades</string>
|
||||
<string name="lbl_props">Propriedades da música</string>
|
||||
|
@ -217,7 +216,6 @@
|
|||
<string name="fmt_indexing">A carregar a sua biblioteca de músicas… (%1$d/%2$d)</string>
|
||||
<string name="set_rewind_prev">Retroceder antes de voltar</string>
|
||||
<string name="desc_exit">Parar reprodução</string>
|
||||
<string name="lbl_relative_path">Caminho principal</string>
|
||||
<string name="set_round_mode_desc">Ativar cantos arredondados em elementos adicionais da interface do utilizador (requer que as capas dos álbuns sejam arredondadas)</string>
|
||||
<string name="fmt_selected">%d Selecionadas</string>
|
||||
<string name="lbl_mixes">Misturas DJ</string>
|
||||
|
|
|
@ -103,10 +103,8 @@
|
|||
<string name="lbl_equalizer">Egalizator</string>
|
||||
<string name="lbl_bitrate">Bit rate</string>
|
||||
<string name="lbl_date_added">Data adăugării</string>
|
||||
<string name="lbl_relative_path">Calea principală</string>
|
||||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_props">Proprietățile cântecului</string>
|
||||
<string name="lbl_file_name">Numele fișierului</string>
|
||||
<string name="lbl_shuffle_shortcut_short">Amestecare</string>
|
||||
<string name="lbl_add">Adaugă</string>
|
||||
<string name="lbl_sample_rate">Frecvența de eșantionare</string>
|
||||
|
|
|
@ -157,7 +157,6 @@
|
|||
<string name="set_pre_amp_warning">Внимание: Изменение предусиления на большое положительное значение может привести к появлению искажений на некоторых звуковых дорожках.</string>
|
||||
<string name="lbl_song_detail">Сведения</string>
|
||||
<string name="lbl_props">Свойства трека</string>
|
||||
<string name="lbl_relative_path">Путь</string>
|
||||
<string name="lbl_format">Формат</string>
|
||||
<string name="lbl_size">Размер</string>
|
||||
<string name="lbl_sample_rate">Частота дискретизации</string>
|
||||
|
@ -165,7 +164,6 @@
|
|||
<string name="lbl_library_counts">Статистика библиотеки</string>
|
||||
<string name="set_restore_state">Восстановить состояние воспроизведения</string>
|
||||
<string name="lbl_duration">Продолжительность</string>
|
||||
<string name="lbl_file_name">Имя файла</string>
|
||||
<string name="lbl_ep">Мини-альбом</string>
|
||||
<string name="lbl_eps">Мини-альбомы</string>
|
||||
<string name="lbl_single">Сингл</string>
|
||||
|
|
|
@ -59,7 +59,6 @@
|
|||
<string name="set_save_desc">Shrani trenutno stanje predvajanja zdaj</string>
|
||||
<string name="desc_skip_prev">Preskoči na zadnjo pesem</string>
|
||||
<string name="set_observing_desc">Ponovno naloži glasbeno knjižnico vsakič, ko se zazna sprememba (zahteva vztrajno obvestilo)</string>
|
||||
<string name="lbl_relative_path">Pot do datoteke</string>
|
||||
<plurals name="fmt_song_count">
|
||||
<item quantity="one">%d pesem</item>
|
||||
<item quantity="two">%d pesmi</item>
|
||||
|
@ -79,7 +78,6 @@
|
|||
<string name="lbl_mixtapes">Mešanice</string>
|
||||
<string name="lbl_artist">Izvajalec</string>
|
||||
<string name="set_intelligent_sorting_desc">Pravilno razvrsti imena, ki se začnejo z številkami ali besedami, kot so \'the\' (najbolje deluje z angleško glasbo)</string>
|
||||
<string name="lbl_file_name">Ime datoteke</string>
|
||||
<string name="clr_teal">Zelenkasto modra</string>
|
||||
<string name="set_state">Vztrajnost</string>
|
||||
<string name="desc_shuffle_all">Premešaj vse pesmi</string>
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
<string name="lbl_song_detail">Visa egenskaper</string>
|
||||
<string name="lbl_share">Dela</string>
|
||||
<string name="lbl_props">Egenskaper för låt</string>
|
||||
<string name="lbl_relative_path">Överordnad mapp</string>
|
||||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_size">Storlek</string>
|
||||
<string name="lbl_sample_rate">Samplingsfrekvens</string>
|
||||
|
@ -99,7 +98,6 @@
|
|||
<string name="lbl_disc">Disk</string>
|
||||
<string name="lbl_sort">Sortera</string>
|
||||
<string name="lbl_queue_add">Lägg till kö</string>
|
||||
<string name="lbl_file_name">Filnamn</string>
|
||||
<string name="lbl_add">Lägg till</string>
|
||||
<string name="lbl_state_wiped">Tillstånd tog bort</string>
|
||||
<string name="lbl_bitrate">Bithastighet</string>
|
||||
|
|
|
@ -52,10 +52,8 @@
|
|||
<item quantity="one">%d albüm</item>
|
||||
<item quantity="other">%d albümler</item>
|
||||
</plurals>
|
||||
<string name="lbl_file_name">Dosya adı</string>
|
||||
<string name="lbl_song_detail">Özellikleri görüntüle</string>
|
||||
<string name="lbl_props">Şarkı özellikleri</string>
|
||||
<string name="lbl_relative_path">Ana yol</string>
|
||||
<string name="lbl_format">Biçim</string>
|
||||
<string name="lbl_shuffle_shortcut_short">Karıştır</string>
|
||||
<string name="lbl_shuffle_shortcut_long">Hepsini karıştır</string>
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
<item quantity="many">%d альбомів</item>
|
||||
<item quantity="other">%d альбомів</item>
|
||||
</plurals>
|
||||
<string name="lbl_file_name">Ім\'я файлу</string>
|
||||
<string name="lbl_format">Формат</string>
|
||||
<string name="lbl_ok">Добре</string>
|
||||
<string name="lbl_cancel">Скасувати</string>
|
||||
|
@ -79,7 +78,6 @@
|
|||
<string name="lbl_compilations">Збірки</string>
|
||||
<string name="lbl_compilation">Збірка</string>
|
||||
<string name="lbl_album_live">Концертний альбом</string>
|
||||
<string name="lbl_relative_path">Шлях до каталогу</string>
|
||||
<string name="set_display">Екран</string>
|
||||
<string name="lbl_date">Рік</string>
|
||||
<string name="set_cover_mode">Обкладинки альбомів</string>
|
||||
|
|
|
@ -176,8 +176,6 @@
|
|||
<string name="lbl_track">音轨</string>
|
||||
<string name="lbl_song_detail">查看属性</string>
|
||||
<string name="lbl_props">曲目属性</string>
|
||||
<string name="lbl_file_name">文件名</string>
|
||||
<string name="lbl_relative_path">上级目录</string>
|
||||
<string name="lbl_format">格式</string>
|
||||
<string name="lbl_size">大小</string>
|
||||
<string name="lbl_bitrate">比特率</string>
|
||||
|
|
|
@ -135,8 +135,7 @@
|
|||
<string name="lbl_share">Share</string>
|
||||
|
||||
<string name="lbl_props">Song properties</string>
|
||||
<string name="lbl_file_name">File name</string>
|
||||
<string name="lbl_relative_path">Parent path</string>
|
||||
<string name="lbl_path">Path</string>
|
||||
<!-- As in audio format -->
|
||||
<string name="lbl_format">Format</string>
|
||||
<!-- As in file size -->
|
||||
|
|
Loading…
Reference in a new issue