Merge pull request #713 from OxygenCobalt/dev

Version 3.4.0
This commit is contained in:
Alexander Capehart 2024-02-17 14:43:45 -07:00 committed by GitHub
commit 5517a65048
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 2580 additions and 1616 deletions

View file

@ -1,5 +1,26 @@
# Changelog
## 3.4.0
#### What's New
- Gapless playback is now used whenever possible
- Added "Remember pause" setting that makes remain paused when skipping
or editing queue
- Added 1x4 and 1x3 widget forms
#### What's Fixed
- Increased music timeout to 60 seconds to accomodate large cover arts
on slow storage drives
- Fixed app repeatedly crashing when automatic theme was on
#### What's Improved
- The playback state is now saved more often, improving persistence
- The queue is now fully circular when repeat all is enabled
#### What's Changed
- You can no longer save, restore, or clear the playback state
- The playback session now ends if you swipe away the app while it's paused
## 3.3.3
#### What's Fixed

View file

@ -2,8 +2,8 @@
<h1 align="center"><b>Auxio</b></h1>
<h4 align="center">A simple, rational music player for android.</h4>
<p align="center">
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.3.3">
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.3.3&color=64B5F6&style=flat">
<a href="https://github.com/oxygencobalt/Auxio/releases/tag/v3.4.0">
<img alt="Latest Version" src="https://img.shields.io/static/v1?label=tag&message=v3.4.0&color=64B5F6&style=flat">
</a>
<a href="https://github.com/oxygencobalt/Auxio/releases/">
<img alt="Releases" src="https://img.shields.io/github/downloads/OxygenCobalt/Auxio/total.svg?color=4B95DE&style=flat">
@ -51,6 +51,7 @@ precise/original dates, sort tags, and more
- SD Card-aware folder management
- Reliable playlisting functionality
- Playback state persistence
- Automatic gapless playback
- Full ReplayGain support (On MP3, FLAC, OGG, OPUS, and MP4 files)
- External equalizer support (ex. Wavelet)
- Edge-to-edge
@ -65,19 +66,24 @@ precise/original dates, sort tags, and more
- Storage (`READ_MEDIA_AUDIO`, `READ_EXTERNAL_STORAGE`) to read and play your music files
- Services (`FOREGROUND_SERVICE`, `WAKE_LOCK`) to keep the music playing in the background
- Notifcations (`POST_NOTIFICATION`) to indicate ongoing playback and music loading
## Donate
You can support Auxio's development through [my Github Sponsors page](https://github.com/sponsors/OxygenCobalt). Get the ability to prioritize features and have your profile added to the README, Release Changelogs, and even App Itself!
You can support Auxio's development through [my Github Sponsors page](https://github.com/sponsors/OxygenCobalt). Get the ability to prioritize features and have your profile added to the README, Release Changelogs, and even the app itself!
**$16/month supporters:**
<p align="center"><b>$16/month supporters:</b></p>
*Be the first to have their profile picture and username added here!*
<p align="center">
<a href="https://github.com/yrliet"><img src="https://avatars.githubusercontent.com/u/151430565?v=4" width=100 /><p align="center"><b><a href="https://github.com/yrliet">yrliet</a></b></p></a>
</p>
**$8/month supporters:**
<p align="center"><b>$8/month supporters:</b></p>
<p align="start">
<p align="center">
<a href="https://github.com/alanorth"><img src="https://avatars.githubusercontent.com/u/191754?v=4" width=50 /></a>
<a href="https://github.com/dmint789"><img src="https://avatars.githubusercontent.com/u/53250435?v=4" width=50 /></a>
<a href="https://github.com/gtsiam"><img src="https://avatars.githubusercontent.com/u/7459196?v=4" width=50 /></a>
</p>
## Building

View file

@ -21,8 +21,8 @@ android {
defaultConfig {
applicationId namespace
versionName "3.3.3"
versionCode 40
versionName "3.4.0"
versionCode 41
minSdk 24
targetSdk 34

View file

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Bluetooth auto-connect functionality (Disabled until permission workflow can be made) -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />-->

View file

@ -31,7 +31,7 @@ import javax.inject.Inject
import org.oxycblt.auxio.databinding.ActivityMainBinding
import org.oxycblt.auxio.music.system.IndexerService
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.system.PlaybackService
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.isNight
@ -76,7 +76,7 @@ class MainActivity : AppCompatActivity() {
if (!startIntentAction(intent)) {
// No intent action to do, just restore the previously saved state.
playbackModel.startAction(InternalPlayer.Action.RestoreState)
playbackModel.playDeferred(DeferredPlayback.RestoreState)
}
}
@ -111,12 +111,12 @@ class MainActivity : AppCompatActivity() {
}
/**
* Transform an [Intent] given to [MainActivity] into a [InternalPlayer.Action] that can be used
* in the playback system.
* Transform an [Intent] given to [MainActivity] into a [DeferredPlayback] that can be used in
* the playback system.
*
* @param intent The (new) [Intent] given to this [MainActivity], or null if there is no intent.
* @return true If the analogous [InternalPlayer.Action] to the given [Intent] was started,
* false otherwise.
* @return true If the analogous [DeferredPlayback] to the given [Intent] was started, false
* otherwise.
*/
private fun startIntentAction(intent: Intent?): Boolean {
if (intent == null) {
@ -137,15 +137,15 @@ class MainActivity : AppCompatActivity() {
val action =
when (intent.action) {
Intent.ACTION_VIEW -> InternalPlayer.Action.Open(intent.data ?: return false)
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> InternalPlayer.Action.ShuffleAll
Intent.ACTION_VIEW -> DeferredPlayback.Open(intent.data ?: return false)
Auxio.INTENT_KEY_SHORTCUT_SHUFFLE -> DeferredPlayback.ShuffleAll
else -> {
logW("Unexpected intent ${intent.action}")
return false
}
}
logD("Translated intent to $action")
playbackModel.startAction(action)
playbackModel.playDeferred(action)
return true
}

View file

@ -148,19 +148,6 @@ class HomeFragment :
// --- UI SETUP ---
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
// it ourselves.
binding.root.rootView.apply {
post {
findViewById<View>(R.id.main_scrim).setOnTouchListener { _, event ->
handleSpeedDialBoundaryTouch(event)
}
findViewById<View>(R.id.sheet_scrim).setOnTouchListener { _, event ->
handleSpeedDialBoundaryTouch(event)
}
}
}
binding.homeAppbar.addOnOffsetChangedListener(this)
binding.homeNormalToolbar.apply {
setOnMenuItemClickListener(this@HomeFragment)
@ -235,6 +222,23 @@ class HomeFragment :
collect(detailModel.toShow.flow, ::handleShow)
}
override fun onStart() {
super.onStart()
// Stock bottom sheet overlay won't work with our nested UI setup, have to replicate
// it ourselves.
requireBinding().root.rootView.apply {
post {
findViewById<View>(R.id.main_scrim).setOnTouchListener { _, event ->
handleSpeedDialBoundaryTouch(event)
}
findViewById<View>(R.id.sheet_scrim).setOnTouchListener { _, event ->
handleSpeedDialBoundaryTouch(event)
}
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
val transition = enterTransition
if (transition is MaterialSharedAxis) {

View file

@ -244,7 +244,7 @@ class ThemedSpeedDialView : SpeedDialView {
companion object {
private val VIEW_PROPERTY_BACKGROUND_TINT =
object : Property<View, Int>(Int::class.java, "backgroundTint") {
override fun get(view: View): Int? = view.backgroundTintList!!.defaultColor
override fun get(view: View): Int = view.backgroundTintList!!.defaultColor
override fun set(view: View, value: Int?) {
view.backgroundTintList = ColorStateList.valueOf(value!!)
@ -253,7 +253,7 @@ class ThemedSpeedDialView : SpeedDialView {
private val IMAGE_VIEW_PROPERTY_IMAGE_TINT =
object : Property<ImageView, Int>(Int::class.java, "imageTint") {
override fun get(view: ImageView): Int? = view.imageTintList!!.defaultColor
override fun get(view: ImageView): Int = view.imageTintList!!.defaultColor
override fun set(view: ImageView, value: Int?) {
view.imageTintList = ColorStateList.valueOf(value!!)

View file

@ -42,6 +42,7 @@ import org.oxycblt.auxio.music.metadata.Separators
import org.oxycblt.auxio.music.metadata.TagExtractor
import org.oxycblt.auxio.music.user.MutableUserLibrary
import org.oxycblt.auxio.music.user.UserLibrary
import org.oxycblt.auxio.util.DEFAULT_TIMEOUT
import org.oxycblt.auxio.util.forEachWithTimeout
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
@ -481,7 +482,7 @@ constructor(
val rawSongs = LinkedList<RawSong>()
// Use a longer timeout so that dependent components can timeout and throw errors that
// provide more context than if we timed out here.
processedSongs.forEachWithTimeout(20000) {
processedSongs.forEachWithTimeout(DEFAULT_TIMEOUT * 2) {
rawSongs.add(it)
// Since discovery takes up the bulk of the music loading process, we switch to
// indicating a defined amount of loaded songs in comparison to the projected amount
@ -489,7 +490,7 @@ constructor(
emitIndexingProgress(IndexingProgress.Songs(rawSongs.size, query.projectedTotal))
}
withTimeout(10000) {
withTimeout(DEFAULT_TIMEOUT) {
mediaStoreJob.await()
tagJob.await()
}

View file

@ -57,8 +57,10 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
class MusicSettingsImpl
@Inject
constructor(@ApplicationContext context: Context, val documentPathFactory: DocumentPathFactory) :
Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
constructor(
@ApplicationContext context: Context,
private val documentPathFactory: DocumentPathFactory
) : Settings.Impl<MusicSettings.Listener>(context), MusicSettings {
private val storageManager = context.getSystemServiceCompat(StorageManager::class)
override var musicDirs: MusicDirectories

View file

@ -368,7 +368,8 @@ sealed interface PlaylistDecision {
* @param songs The [Song]s to contain in the new [Playlist].
* @param template An existing playlist name that should be editable in the opened dialog. If
* null, a placeholder should be created and shown as a hint instead.
* @param context The context in which this decision is being fulfilled.
* @param reason The reason why a new playlist is being created. For all intensive purposes, you
* do not need to specify this.
*/
data class New(val songs: List<Song>, val template: String?, val reason: Reason) :
PlaylistDecision {

View file

@ -210,7 +210,7 @@ class PlaylistPickerViewModel @Inject constructor(private val musicRepository: M
}
/**
* Set a new [currentPlaylisttoExport] from a [Playlist] [Music.UID].
* Set a new [currentPlaylistToExport] from a [Playlist] [Music.UID].
*
* @param playlistUid The [Music.UID] of the [Playlist] to export.
*/

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.util.inflater
import org.oxycblt.auxio.util.logD
/**
* [RecyclerView.Adapter] that manages a list of [Directory] instances.
* [RecyclerView.Adapter] that manages a list of [Path] music directory instances.
*
* @param listener A [DirectoryAdapter.Listener] to bind interactions to.
* @author Alexander Capehart (OxygenCobalt)
@ -37,9 +37,7 @@ import org.oxycblt.auxio.util.logD
class DirectoryAdapter(private val listener: Listener) :
RecyclerView.Adapter<MusicDirViewHolder>() {
private val _dirs = mutableListOf<Path>()
/**
* The current list of [SystemPath]s, may not line up with [MusicDirectories] due to removals.
*/
/** The current list of [Path]s, may not line up with [MusicDirectories] due to removals. */
val dirs: List<Path> = _dirs
override fun getItemCount() = dirs.size
@ -94,7 +92,7 @@ class DirectoryAdapter(private val listener: Listener) :
}
/**
* A [RecyclerView.Recycler] that displays a [Directory]. Use [from] to create an instance.
* A [RecyclerView.Recycler] that displays a [Path]. Use [from] to create an instance.
*
* @author Alexander Capehart (OxygenCobalt)
*/

View file

@ -23,9 +23,10 @@ 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.
* @param dirs A list of directory [Path] instances. How these are interpreted depends on
* [shouldInclude]
* @param shouldInclude True if the library should only load from the [Path] instances, false if the
* library should not load from the [Path] instances.
* @author Alexander Capehart (OxygenCobalt)
*/
data class MusicDirectories(val dirs: List<Path>, val shouldInclude: Boolean)

View file

@ -90,7 +90,7 @@ interface MediaStoreExtractor {
* Create a framework-backed instance.
*
* @param context [Context] required.
* @param volumeManager [VolumeManager] required.
* @param pathInterpreterFactory A [MediaStorePathInterpreter.Factory] to use.
* @return A new [MediaStoreExtractor] that will work best on the device's API level.
*/
fun from(

View file

@ -35,7 +35,6 @@ import kotlinx.coroutines.Job
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.music.IndexingProgress
import org.oxycblt.auxio.music.IndexingState
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.MusicSettings
import org.oxycblt.auxio.music.fs.contentResolverSafe
@ -144,15 +143,11 @@ class IndexerService :
// the listener system of another.
playbackManager.toSavedState()?.let { savedState ->
playbackManager.applySavedState(
PlaybackStateManager.SavedState(
parent =
savedState.parent?.let { musicRepository.find(it.uid) as? MusicParent },
queueState =
savedState.queueState.remap { song ->
deviceLibrary.findSong(requireNotNull(song).uid)
},
positionMs = savedState.positionMs,
repeatMode = savedState.repeatMode),
savedState.copy(
heap =
savedState.heap.map { song ->
song?.let { deviceLibrary.findSong(it.uid) }
}),
true)
}
}

View file

@ -58,6 +58,8 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
val rewindWithPrev: Boolean
/** Whether a song should pause after every repeat. */
val pauseOnRepeat: Boolean
/** Whether to maintain the play/pause state when skipping or editing the queue */
val rememberPause: Boolean
interface Listener {
/** Called when one of the ReplayGain configurations have changed. */
@ -66,6 +68,8 @@ interface PlaybackSettings : Settings<PlaybackSettings.Listener> {
fun onNotificationActionChanged() {}
/** Called when [barAction] has changed. */
fun onBarActionChanged() {}
/** Called when [pauseOnRepeat] has changed. */
fun onPauseOnRepeatChanged() {}
}
}
@ -127,6 +131,9 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont
override val pauseOnRepeat: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_repeat_pause), false)
override val rememberPause: Boolean
get() = sharedPreferences.getBoolean(getString(R.string.set_key_remember_pause), false)
override fun migrate() {
// MusicMode was converted to PlaySong in 3.2.0
fun Int.migrateMusicMode() =
@ -187,6 +194,10 @@ class PlaybackSettingsImpl @Inject constructor(@ApplicationContext context: Cont
logD("Dispatching bar action change")
listener.onBarActionChanged()
}
getString(R.string.set_key_repeat_pause) -> {
logD("Dispatching pause on repeat change")
listener.onPauseOnRepeatChanged()
}
}
}

View file

@ -36,9 +36,10 @@ import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Playlist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
@ -113,8 +114,7 @@ constructor(
get() = _playbackDecision
/**
* The current audio session ID of the internal player. Null if no [InternalPlayer] is
* available.
* The current audio session ID of the internal player. Null if no audio player is available.
*/
val currentAudioSessionId: Int?
get() = playbackManager.currentAudioSessionId
@ -129,50 +129,55 @@ constructor(
playbackSettings.unregisterListener(this)
}
override fun onIndexMoved(queue: Queue) {
override fun onIndexMoved(index: Int) {
logD("Index moved, updating current song")
_song.value = queue.currentSong
_song.value = playbackManager.currentSong
}
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
// Other types of queue changes preserve the current song.
if (change.type == Queue.Change.Type.SONG) {
if (change.type == QueueChange.Type.SONG) {
logD("Queue changed, updating current song")
_song.value = queue.currentSong
_song.value = playbackManager.currentSong
}
}
override fun onQueueReordered(queue: Queue) {
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {
logD("Queue completely changed, updating current song")
_isShuffled.value = queue.isShuffled
_isShuffled.value = isShuffled
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
logD("New playback started, updating playback information")
_song.value = queue.currentSong
_song.value = playbackManager.currentSong
_parent.value = parent
_isShuffled.value = queue.isShuffled
_isShuffled.value = isShuffled
}
override fun onStateChanged(state: InternalPlayer.State) {
override fun onProgressionChanged(progression: Progression) {
logD("Player state changed, starting new position polling")
_isPlaying.value = state.isPlaying
_isPlaying.value = progression.isPlaying
// Still need to update the position now due to co-routine launch delays
_positionDs.value = state.calculateElapsedPositionMs().msToDs()
_positionDs.value = progression.calculateElapsedPositionMs().msToDs()
// Replace the previous position co-routine with a new one that uses the new
// state information.
lastPositionJob?.cancel()
lastPositionJob =
viewModelScope.launch {
while (true) {
_positionDs.value = state.calculateElapsedPositionMs().msToDs()
_positionDs.value = progression.calculateElapsedPositionMs().msToDs()
// Wait a deci-second for the next position tick.
delay(100)
}
}
}
override fun onRepeatChanged(repeatMode: RepeatMode) {
override fun onRepeatModeChanged(repeatMode: RepeatMode) {
_repeatMode.value = repeatMode
}
@ -223,8 +228,7 @@ constructor(
playFromGenreImpl(song, genre, isImplicitlyShuffled())
}
private fun isImplicitlyShuffled() =
playbackManager.queue.isShuffled && playbackSettings.keepShuffle
private fun isImplicitlyShuffled() = playbackManager.isShuffled && playbackSettings.keepShuffle
private fun playWithImpl(song: Song, with: PlaySong, shuffled: Boolean) {
when (with) {
@ -411,14 +415,14 @@ constructor(
}
/**
* Start the given [InternalPlayer.Action] to be completed eventually. This can be used to
* enqueue a playback action at startup to then occur when the music library is fully loaded.
* Start the given [DeferredPlayback] to be completed eventually. This can be used to enqueue a
* playback action at startup to then occur when the music library is fully loaded.
*
* @param action The [InternalPlayer.Action] to perform eventually.
* @param action The [DeferredPlayback] to perform eventually.
*/
fun startAction(action: InternalPlayer.Action) {
fun playDeferred(action: DeferredPlayback) {
logD("Starting action $action")
playbackManager.startAction(action)
playbackManager.playDeferred(action)
}
// --- PLAYER FUNCTIONS ---
@ -572,13 +576,13 @@ constructor(
/** Toggle [isPlaying] (i.e from playing to paused) */
fun togglePlaying() {
logD("Toggling playing state")
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
playbackManager.playing(!playbackManager.progression.isPlaying)
}
/** Toggle [isShuffled] (ex. from on to off) */
fun toggleShuffled() {
logD("Toggling shuffled state")
playbackManager.reorder(!playbackManager.queue.isShuffled)
playbackManager.shuffled(!playbackManager.isShuffled)
}
/**
@ -588,7 +592,7 @@ constructor(
*/
fun toggleRepeatMode() {
logD("Toggling repeat mode")
playbackManager.repeatMode = playbackManager.repeatMode.increment()
playbackManager.repeatMode(playbackManager.repeatMode.increment())
}
// --- UI CONTROL ---

View file

@ -37,8 +37,8 @@ import org.oxycblt.auxio.playback.state.RepeatMode
* @author Alexander Capehart
*/
@Database(
entities = [PlaybackState::class, QueueHeapItem::class, QueueMappingItem::class],
version = 32,
entities = [PlaybackState::class, QueueHeapItem::class, QueueShuffledMappingItem::class],
version = 38,
exportSchema = false)
@TypeConverters(Music.UID.TypeConverters::class)
abstract class PersistenceDatabase : RoomDatabase() {
@ -109,15 +109,16 @@ interface QueueDao {
/**
* Get the previously persisted queue mapping.
*
* @return A list of persisted [QueueMappingItem]s wrapping each heap item.
* @return A list of persisted [QueueShuffledMappingItem]s wrapping each heap item.
*/
@Query("SELECT * FROM QueueMappingItem") suspend fun getMapping(): List<QueueMappingItem>
@Query("SELECT * FROM QueueShuffledMappingItem")
suspend fun getShuffledMapping(): List<QueueShuffledMappingItem>
/** Delete any previously persisted queue heap entries. */
@Query("DELETE FROM QueueHeapItem") suspend fun nukeHeap()
/** Delete any previously persisted queue mapping entries. */
@Query("DELETE FROM QueueMappingItem") suspend fun nukeMapping()
@Query("DELETE FROM QueueShuffledMappingItem") suspend fun nukeShuffledMapping()
/**
* Insert new heap entries into the database.
@ -129,10 +130,10 @@ interface QueueDao {
/**
* Insert new mapping entries into the database.
*
* @param mapping The list of wrapped [QueueMappingItem] to insert.
* @param mapping The list of wrapped [QueueShuffledMappingItem] to insert.
*/
@Insert(onConflict = OnConflictStrategy.ABORT)
suspend fun insertMapping(mapping: List<QueueMappingItem>)
suspend fun insertShuffledMapping(mapping: List<QueueShuffledMappingItem>)
}
// TODO: Figure out how to get RepeatMode to map to an int instead of a string
@ -148,5 +149,4 @@ data class PlaybackState(
@Entity data class QueueHeapItem(@PrimaryKey val id: Int, val uid: Music.UID)
@Entity
data class QueueMappingItem(@PrimaryKey val id: Int, val orderedIndex: Int, val shuffledIndex: Int)
@Entity data class QueueShuffledMappingItem(@PrimaryKey val id: Int, val index: Int)

View file

@ -21,7 +21,6 @@ package org.oxycblt.auxio.playback.persist
import javax.inject.Inject
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
@ -54,46 +53,37 @@ constructor(
override suspend fun readState(): PlaybackStateManager.SavedState? {
val deviceLibrary = musicRepository.deviceLibrary ?: return null
val playbackState: PlaybackState
val heap: List<QueueHeapItem>
val mapping: List<QueueMappingItem>
val heapItems: List<QueueHeapItem>
val mappingItems: List<QueueShuffledMappingItem>
try {
playbackState = playbackStateDao.getState() ?: return null
heap = queueDao.getHeap()
mapping = queueDao.getMapping()
heapItems = queueDao.getHeap()
mappingItems = queueDao.getShuffledMapping()
} catch (e: Exception) {
logE("Unable read playback state")
logE(e.stackTraceToString())
return null
}
val orderedMapping = mutableListOf<Int>()
val shuffledMapping = mutableListOf<Int>()
for (entry in mapping) {
orderedMapping.add(entry.orderedIndex)
shuffledMapping.add(entry.shuffledIndex)
}
val heap = heapItems.map { deviceLibrary.findSong(it.uid) }
val shuffledMapping = mappingItems.map { it.index }
val parent = playbackState.parentUid?.let { musicRepository.find(it) as? MusicParent }
logD("Successfully read playback state")
return PlaybackStateManager.SavedState(
parent = parent,
queueState =
Queue.SavedState(
heap.map { deviceLibrary.findSong(it.uid) },
orderedMapping,
shuffledMapping,
playbackState.index,
playbackState.songUid),
positionMs = playbackState.positionMs,
repeatMode = playbackState.repeatMode)
repeatMode = playbackState.repeatMode,
parent = parent,
heap = heap,
shuffledMapping = shuffledMapping,
index = playbackState.index,
songUid = playbackState.songUid)
}
override suspend fun saveState(state: PlaybackStateManager.SavedState?): Boolean {
try {
playbackStateDao.nukeState()
queueDao.nukeHeap()
queueDao.nukeMapping()
queueDao.nukeShuffledMapping()
} catch (e: Exception) {
logE("Unable to clear previous state")
logE(e.stackTraceToString())
@ -106,29 +96,23 @@ constructor(
val playbackState =
PlaybackState(
id = 0,
index = state.queueState.index,
index = state.index,
positionMs = state.positionMs,
repeatMode = state.repeatMode,
songUid = state.queueState.songUid,
songUid = state.songUid,
parentUid = state.parent?.uid)
// Convert the remaining queue information do their database-specific counterparts.
val heap =
state.queueState.heap.mapIndexed { i, song ->
QueueHeapItem(i, requireNotNull(song).uid)
}
state.heap.mapIndexed { i, song -> QueueHeapItem(i, requireNotNull(song).uid) }
val mapping =
state.queueState.orderedMapping.zip(state.queueState.shuffledMapping).mapIndexed {
i,
pair ->
QueueMappingItem(i, pair.first, pair.second)
}
val shuffledMapping =
state.shuffledMapping.mapIndexed { i, index -> QueueShuffledMappingItem(i, index) }
try {
playbackStateDao.insertState(playbackState)
queueDao.insertHeap(heap)
queueDao.insertMapping(mapping)
queueDao.insertShuffledMapping(shuffledMapping)
} catch (e: Exception) {
logE("Unable to write new state")
logE(e.stackTraceToString())

View file

@ -1,449 +0,0 @@
/*
* Copyright (c) 2023 Auxio Project
* Queue.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.playback.queue
import kotlin.random.Random
import kotlin.random.nextInt
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.util.logD
/**
* A heap-backed play queue.
*
* Whereas other queue implementations use a plain list, Auxio requires a more complicated data
* structure in order to implement features such as gapless playback in ExoPlayer. This queue
* implementation is instead based around an unorganized "heap" of [Song] instances, that are then
* interpreted into different queues depending on the current playback configuration.
*
* In general, the implementation details don't need to be known for this data structure to be used,
* except in special circumstances like [SavedState]. The functions exposed should be familiar for
* any typical play queue.
*
* @author OxygenCobalt
*/
interface Queue {
/** The index of the currently playing [Song] in the current mapping. */
val index: Int
/** The currently playing [Song]. */
val currentSong: Song?
/** Whether this queue is shuffled. */
val isShuffled: Boolean
/**
* Resolve this queue into a more conventional list of [Song]s.
*
* @return A list of [Song] corresponding to the current queue mapping.
*/
fun resolve(): List<Song>
/**
* Represents the possible changes that can occur during certain queue mutation events.
*
* @param type The [Type] of the change to the internal queue state.
* @param instructions The update done to the resolved queue list.
*/
data class Change(val type: Type, val instructions: UpdateInstructions) {
enum class Type {
/** Only the mapping has changed. */
MAPPING,
/** The mapping has changed, and the index also changed to align with it. */
INDEX,
/**
* The current song has changed, possibly alongside the mapping and index depending on
* the context.
*/
SONG
}
}
/**
* An immutable representation of the queue state.
*
* @param heap The heap of [Song]s that are/were used in the queue. This can be modified with
* null values to represent [Song]s that were "lost" from the heap without having to change
* other values.
* @param orderedMapping The mapping of the [heap] to an ordered queue.
* @param shuffledMapping The mapping of the [heap] to a shuffled queue.
* @param index The index of the currently playing [Song] at the time of serialization.
* @param songUid The [Music.UID] of the [Song] that was originally at [index].
*/
class SavedState(
val heap: List<Song?>,
val orderedMapping: List<Int>,
val shuffledMapping: List<Int>,
val index: Int,
val songUid: Music.UID,
) {
/**
* Remaps the [heap] of this instance based on the given mapping function and copies it into
* a new [SavedState].
*
* @param transform Code to remap the existing [Song] heap into a new [Song] heap. This
* **MUST** be the same size as the original heap. [Song] instances that could not be
* converted should be replaced with null in the new heap.
* @throws IllegalStateException If the invariant specified by [transform] is violated.
*/
inline fun remap(transform: (Song?) -> Song?) =
SavedState(heap.map(transform), orderedMapping, shuffledMapping, index, songUid)
}
}
class MutableQueue : Queue {
@Volatile private var heap = mutableListOf<Song>()
@Volatile private var orderedMapping = mutableListOf<Int>()
@Volatile private var shuffledMapping = mutableListOf<Int>()
@Volatile
override var index = -1
private set
override val currentSong: Song?
get() =
shuffledMapping
.ifEmpty { orderedMapping.ifEmpty { null } }
?.getOrNull(index)
?.let(heap::get)
override val isShuffled: Boolean
get() = shuffledMapping.isNotEmpty()
override fun resolve() =
if (currentSong != null) {
shuffledMapping.map { heap[it] }.ifEmpty { orderedMapping.map { heap[it] } }
} else {
// Queue doesn't exist, return saner data.
listOf()
}
/**
* Go to a particular index in the queue.
*
* @param to The index of the [Song] to start playing, in the current queue mapping.
* @return true if the queue jumped to that position, false otherwise.
*/
fun goto(to: Int): Boolean {
if (to !in orderedMapping.indices) {
return false
}
index = to
return true
}
/**
* Start a new queue configuration.
*
* @param play The [Song] to play, or null to start from a random position.
* @param queue The queue of [Song]s to play. Must contain [play]. This list will become the
* heap internally.
* @param shuffled Whether to shuffle the queue or not. This changes the interpretation of
* [queue].
*/
fun start(play: Song?, queue: List<Song>, shuffled: Boolean) {
heap = queue.toMutableList()
orderedMapping = MutableList(queue.size) { it }
shuffledMapping = mutableListOf()
index =
play?.let(queue::indexOf) ?: if (shuffled) Random.Default.nextInt(queue.indices) else 0
reorder(shuffled)
check()
}
/**
* Re-order the queue.
*
* @param shuffled Whether the queue should be shuffled or not.
*/
fun reorder(shuffled: Boolean) {
if (orderedMapping.isEmpty()) {
// Nothing to do.
return
}
logD("Reordering queue [shuffled=$shuffled]")
if (shuffled) {
val trueIndex =
if (shuffledMapping.isNotEmpty()) {
// Re-shuffling, song to preserve is in the shuffled mapping
shuffledMapping[index]
} else {
// First shuffle, song to preserve is in the ordered mapping
orderedMapping[index]
}
// Since we are re-shuffling existing songs, we use the previous mapping size
// instead of the total queue size.
shuffledMapping = orderedMapping.shuffled().toMutableList()
shuffledMapping.add(0, shuffledMapping.removeAt(shuffledMapping.indexOf(trueIndex)))
index = 0
} else if (shuffledMapping.isNotEmpty()) {
// Ordering queue, song to preserve is in the shuffled mapping.
index = orderedMapping.indexOf(shuffledMapping[index])
shuffledMapping = mutableListOf()
}
check()
}
/**
* Add [Song]s to the "top" of the queue (right next to the currently playing song). Will start
* playback if nothing is playing.
*
* @param songs The [Song]s to add.
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun addToTop(songs: List<Song>): Queue.Change {
logD("Adding ${songs.size} songs to the front of the queue")
val insertAt = index + 1
val heapIndices = songs.map(::addSongToHeap)
if (shuffledMapping.isNotEmpty()) {
// Add the new songs in front of the current index in the shuffled mapping and in front
// of the analogous list song in the ordered mapping.
logD("Must append songs to shuffled mapping")
val orderedIndex = orderedMapping.indexOf(shuffledMapping[index])
orderedMapping.addAll(orderedIndex + 1, heapIndices)
shuffledMapping.addAll(insertAt, heapIndices)
} else {
// Add the new song in front of the current index in the ordered mapping.
logD("Only appending songs to ordered mapping")
orderedMapping.addAll(insertAt, heapIndices)
}
check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))
}
/**
* Add [Song]s to the end of the queue. Will start playback if nothing is playing.
*
* @param songs The [Song]s to add.
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun addToBottom(songs: List<Song>): Queue.Change {
logD("Adding ${songs.size} songs to the back of the queue")
val insertAt = orderedMapping.size
val heapIndices = songs.map(::addSongToHeap)
// Can simple append the new songs to the end of both mappings.
orderedMapping.addAll(heapIndices)
if (shuffledMapping.isNotEmpty()) {
logD("Appending songs to shuffled mapping")
shuffledMapping.addAll(heapIndices)
}
check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Add(insertAt, songs.size))
}
/**
* Move a [Song] at the given position to a new position.
*
* @param src The position of the [Song] to move.
* @param dst The destination position of the [Song].
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun move(src: Int, dst: Int): Queue.Change {
if (shuffledMapping.isNotEmpty()) {
// Move songs only in the shuffled mapping. There is no sane analogous form of
// this for the ordered mapping.
shuffledMapping.add(dst, shuffledMapping.removeAt(src))
} else {
// Move songs in the ordered mapping.
orderedMapping.add(dst, orderedMapping.removeAt(src))
}
val oldIndex = index
when (index) {
// We are moving the currently playing song, correct the index to it's new position.
src -> {
logD("Moving current song, shifting index")
index = dst
}
// We have moved an song from behind the playing song to in front, shift back.
in (src + 1)..dst -> {
logD("Moving song from behind -> front, shift backwards")
index -= 1
}
// We have moved an song from in front of the playing song to behind, shift forward.
in dst until src -> {
logD("Moving song from front -> behind, shift forward")
index += 1
}
else -> {
// Nothing to do.
logD("Move preserved index")
check()
return Queue.Change(Queue.Change.Type.MAPPING, UpdateInstructions.Move(src, dst))
}
}
logD("Move changed index: $oldIndex -> $index")
check()
return Queue.Change(Queue.Change.Type.INDEX, UpdateInstructions.Move(src, dst))
}
/**
* Remove a [Song] at the given position.
*
* @param at The position of the [Song] to remove.
* @return A [Queue.Change] instance that reflects the changes made.
*/
fun remove(at: Int): Queue.Change {
val lastIndex = orderedMapping.lastIndex
if (shuffledMapping.isNotEmpty()) {
// Remove the specified index in the shuffled mapping and the analogous song in the
// ordered mapping.
orderedMapping.removeAt(orderedMapping.indexOf(shuffledMapping[at]))
shuffledMapping.removeAt(at)
} else {
// Remove the specified index in the shuffled mapping
orderedMapping.removeAt(at)
}
// Note: We do not clear songs out from the heap, as that would require the backing data
// of the player to be completely invalidated. It's generally easier to not remove the
// song and retain player state consistency.
val type =
when {
// We just removed the currently playing song.
index == at -> {
logD("Removed current song")
if (lastIndex == index) {
logD("Current song at end of queue, shift back")
--index
}
Queue.Change.Type.SONG
}
// Index was ahead of removed song, shift back to preserve consistency.
index > at -> {
logD("Removed before current song, shift back")
--index
Queue.Change.Type.INDEX
}
// Nothing to do
else -> {
logD("Removal preserved index")
Queue.Change.Type.MAPPING
}
}
logD("Committing change of type $type")
check()
return Queue.Change(type, UpdateInstructions.Remove(at, 1))
}
/**
* Convert the current state of this instance into a [Queue.SavedState].
*
* @return A new [Queue.SavedState] reflecting the exact state of the queue when called.
*/
fun toSavedState() =
currentSong?.let { song ->
Queue.SavedState(
heap.toList(), orderedMapping.toList(), shuffledMapping.toList(), index, song.uid)
}
/**
* Update this instance from the given [Queue.SavedState].
*
* @param savedState A [Queue.SavedState] with a valid queue representation.
*/
fun applySavedState(savedState: Queue.SavedState) {
val adjustments = mutableListOf<Int?>()
var currentShift = 0
for (song in savedState.heap) {
if (song != null) {
adjustments.add(currentShift)
} else {
adjustments.add(null)
currentShift -= 1
}
}
logD("Created adjustment mapping [max shift=$currentShift]")
heap = savedState.heap.filterNotNull().toMutableList()
orderedMapping =
savedState.orderedMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
shuffledMapping =
savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { heapIndex ->
adjustments[heapIndex]?.let { heapIndex + it }
}
// Make sure we re-align the index to point to the previously playing song.
index = savedState.index
while (currentSong?.uid != savedState.songUid && index > -1) {
index--
}
logD("Corrected index: ${savedState.index} -> $index")
check()
}
private fun addSongToHeap(song: Song): Int {
// We want to first try to see if there are any "orphaned" songs in the queue
// that we can re-use. This way, we can reduce the memory used up by songs that
// were previously removed from the queue.
val currentMapping = orderedMapping
if (orderedMapping.isNotEmpty()) {
// While we could iterate through the queue and then check the mapping, it's
// faster if we first check the queue for all instances of this song, and then
// do a exclusion of this set of indices with the current mapping in order to
// obtain the orphaned songs.
val orphanCandidates = mutableSetOf<Int>()
for (entry in heap.withIndex()) {
if (entry.value == song) {
orphanCandidates.add(entry.index)
}
}
logD("Found orphans: ${orphanCandidates.map { heap[it] }}")
orphanCandidates.removeAll(currentMapping.toSet())
if (orphanCandidates.isNotEmpty()) {
val orphan = orphanCandidates.first()
logD("Found an orphan that could be re-used: ${heap[orphan]}")
// There are orphaned songs, return the first one we find.
return orphan
}
}
// Nothing to re-use, add this song to the queue
logD("No orphan could be re-used")
heap.add(song)
return heap.lastIndex
}
private fun check() {
check(!(heap.isEmpty() && (orderedMapping.isNotEmpty() || shuffledMapping.isNotEmpty()))) {
"Queue inconsistency detected: Empty heap with non-empty mappings" +
"[ordered: ${orderedMapping.size}, shuffled: ${shuffledMapping.size}]"
}
check(shuffledMapping.isEmpty() || orderedMapping.size == shuffledMapping.size) {
"Queue inconsistency detected: Ordered mapping size ${orderedMapping.size} " +
"!= Shuffled mapping size ${shuffledMapping.size}"
}
check(orderedMapping.all { it in heap.indices }) {
"Queue inconsistency detected: Ordered mapping indices out of heap bounds"
}
check(shuffledMapping.all { it in heap.indices }) {
"Queue inconsistency detected: Shuffled mapping indices out of heap bounds"
}
}
}

View file

@ -27,6 +27,7 @@ import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.util.Event
import org.oxycblt.auxio.util.MutableEvent
import org.oxycblt.auxio.util.logD
@ -51,7 +52,7 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
val scrollTo: Event<Int>
get() = _scrollTo
private val _index = MutableStateFlow(playbackManager.queue.index)
private val _index = MutableStateFlow(playbackManager.index)
/** The index of the currently playing song in the queue. */
val index: StateFlow<Int>
get() = _index
@ -60,40 +61,45 @@ class QueueViewModel @Inject constructor(private val playbackManager: PlaybackSt
playbackManager.addListener(this)
}
override fun onIndexMoved(queue: Queue) {
override fun onIndexMoved(index: Int) {
logD("Index moved, synchronizing and scrolling to new position")
_scrollTo.put(queue.index)
_index.value = queue.index
_scrollTo.put(index)
_index.value = index
}
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
// Queue changed trivially due to item mo -> Diff queue, stay at current index.
logD("Updating queue display")
_queueInstructions.put(change.instructions)
_queue.value = queue.resolve()
if (change.type != Queue.Change.Type.MAPPING) {
_queue.value = queue
if (change.type != QueueChange.Type.MAPPING) {
// Index changed, make sure it remains updated without actually scrolling to it.
logD("Index changed with queue, synchronizing new position")
_index.value = queue.index
_index.value = index
}
}
override fun onQueueReordered(queue: Queue) {
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {
// Queue changed completely -> Replace queue, update index
logD("Queue changed completely, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(queue.index)
_queue.value = queue.resolve()
_index.value = queue.index
_scrollTo.put(index)
_queue.value = queue
_index.value = index
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
// Entirely new queue -> Replace queue, update index
logD("New playback, replacing queue and position")
_queueInstructions.put(UpdateInstructions.Replace(0))
_scrollTo.put(queue.index)
_queue.value = queue.resolve()
_index.value = queue.index
_scrollTo.put(index)
_queue.value = queue
_index.value = index
}
override fun onCleared() {

View file

@ -30,8 +30,8 @@ import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.util.logD
/**
@ -70,26 +70,31 @@ constructor(
// --- OVERRIDES ---
override fun onIndexMoved(queue: Queue) {
override fun onIndexMoved(index: Int) {
logD("Index moved, updating current song")
applyReplayGain(queue.currentSong)
applyReplayGain(playbackManager.currentSong)
}
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
// Other types of queue changes preserve the current song.
if (change.type == Queue.Change.Type.SONG) {
applyReplayGain(queue.currentSong)
if (change.type == QueueChange.Type.SONG) {
applyReplayGain(playbackManager.currentSong)
}
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
logD("New playback started, updating playback information")
applyReplayGain(queue.currentSong)
applyReplayGain(playbackManager.currentSong)
}
override fun onReplayGainSettingsChanged() {
// ReplayGain config changed, we need to set it up again.
applyReplayGain(playbackManager.queue.currentSong)
applyReplayGain(playbackManager.currentSong)
}
// --- REPLAYGAIN PARSING ---
@ -131,7 +136,7 @@ constructor(
logD("Using dynamic strategy")
gain.album?.takeIf {
playbackManager.parent is Album &&
playbackManager.queue.currentSong?.album == playbackManager.parent
playbackManager.currentSong?.album == playbackManager.parent
}
?: gain.track
}

View file

@ -1,183 +0,0 @@
/*
* Copyright (c) 2022 Auxio Project
* InternalPlayer.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.playback.state
import android.net.Uri
import android.os.SystemClock
import android.support.v4.media.session.PlaybackStateCompat
import org.oxycblt.auxio.music.Song
/**
* An interface for internal audio playback. This can be used to coordinate what occurs in the
* background playback task.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface InternalPlayer {
/** The ID of the audio session started by this instance. */
val audioSessionId: Int
/** Whether the player should rewind before skipping back. */
val shouldRewindWithPrev: Boolean
/**
* Load a new [Song] into the internal player.
*
* @param song The [Song] to load, or null if playback should stop entirely.
* @param play Whether to start playing when the [Song] is loaded.
*/
fun loadSong(song: Song?, play: Boolean)
/**
* Called when an [Action] has been queued and this [InternalPlayer] is available to handle it.
*
* @param action The [Action] to perform.
* @return true if the action was handled, false otherwise.
*/
fun performAction(action: Action): Boolean
/**
* Get a [State] corresponding to the current player state.
*
* @param durationMs The duration of the currently playing track, in milliseconds. Required
* since the internal player cannot obtain an accurate duration itself.
*/
fun getState(durationMs: Long): State
/**
* Seek to a given position in the internal player.
*
* @param positionMs The position to seek to, in milliseconds.
*/
fun seekTo(positionMs: Long)
/**
* Set whether the player should play or not.
*
* @param isPlaying Whether to play or pause the current playback.
*/
fun setPlaying(isPlaying: Boolean)
/** Possible long-running background tasks handled by the background playback task. */
sealed interface Action {
/** Restore the previously saved playback state. */
data object RestoreState : Action
/**
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All"
* shortcut.
*/
data object ShuffleAll : Action
/**
* Start playing an audio file at the given [Uri].
*
* @param uri The [Uri] of the audio file to start playing.
*/
data class Open(val uri: Uri) : Action
}
/**
* A representation of the current state of audio playback. Use [from] to create an instance.
*/
class State
private constructor(
/** Whether the player is actively playing audio or set to play audio in the future. */
val isPlaying: Boolean,
/** Whether the player is actively playing audio in this moment. */
private val isAdvancing: Boolean,
/** The position when this instance was created, in milliseconds. */
private val initPositionMs: Long,
/** The time this instance was created, as a unix epoch timestamp. */
private val creationTime: Long
) {
/**
* Calculate the "real" playback position this instance contains, in milliseconds.
*
* @return If paused, the original position will be returned. Otherwise, it will be the
* original position plus the time elapsed since this state was created.
*/
fun calculateElapsedPositionMs() =
if (isAdvancing) {
initPositionMs + (SystemClock.elapsedRealtime() - creationTime)
} else {
// Not advancing due to buffering or some unrelated pausing, such as
// a transient audio focus change.
initPositionMs
}
/**
* Load this instance into a [PlaybackStateCompat].
*
* @param builder The [PlaybackStateCompat.Builder] to mutate.
* @return The same [PlaybackStateCompat.Builder] for easy chaining.
*/
fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder =
builder.setState(
// State represents the user's preference, not the actual player state.
// Doing this produces a better experience in the media control UI.
if (isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
PlaybackStateCompat.STATE_PAUSED
},
initPositionMs,
if (isAdvancing) {
1f
} else {
// Not advancing, so don't move the position.
0f
},
creationTime)
// Equality ignores the creation time to prevent functionally identical states
// from being non-equal.
override fun equals(other: Any?) =
other is State &&
isPlaying == other.isPlaying &&
isAdvancing == other.isAdvancing &&
initPositionMs == other.initPositionMs
override fun hashCode(): Int {
var result = isPlaying.hashCode()
result = 31 * result + isAdvancing.hashCode()
result = 31 * result + initPositionMs.hashCode()
return result
}
companion object {
/**
* Create a new instance.
*
* @param isPlaying Whether the player is actively playing audio or set to play audio in
* the future.
* @param isAdvancing Whether the player is actively playing audio in this moment.
* @param positionMs The current position of the player.
*/
fun from(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) =
State(
isPlaying,
// Minor sanity check: Make sure that advancing can't occur if already paused.
isPlaying && isAdvancing,
positionMs,
SystemClock.elapsedRealtime())
}
}
}

View file

@ -0,0 +1,384 @@
/*
* Copyright (c) 2024 Auxio Project
* PlaybackStateHolder.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.playback.state
import android.net.Uri
import android.os.SystemClock
import android.support.v4.media.session.PlaybackStateCompat
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
/**
* The designated "source of truth" for the current playback state. Should only be used by
* [PlaybackStateManager], which mirrors a more refined version of the state held here.
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PlaybackStateHolder {
/** The current [Progression] state of the audio player. */
val progression: Progression
/** The current [RepeatMode] of the audio player. */
val repeatMode: RepeatMode
/** The current [MusicParent] being played from. Null if playing from all songs. */
val parent: MusicParent?
/**
* Resolve the current queue state as a [RawQueue].
*
* @return The current queue state.
*/
fun resolveQueue(): RawQueue
/** The current audio session ID of the audio player. */
val audioSessionId: Int
/**
* Applies a completely new playback state to the holder.
*
* @param queue The new queue to use.
* @param start The song to start playback from. Should be in the queue.
* @param parent The parent to play from.
* @param shuffled Whether the queue should be shuffled.
*/
fun newPlayback(queue: List<Song>, start: Song?, parent: MusicParent?, shuffled: Boolean)
/**
* Update the playing state of the audio player.
*
* @param playing Whether the player should be playing audio.
*/
fun playing(playing: Boolean)
/**
* Seek to a position in the current song.
*
* @param positionMs The position to seek to, in milliseconds.
*/
fun seekTo(positionMs: Long)
/**
* Update the repeat mode of the audio player.
*
* @param repeatMode The new repeat mode.
*/
fun repeatMode(repeatMode: RepeatMode)
/** Go to the next song in the queue. */
fun next()
/** Go to the previous song in the queue. */
fun prev()
/**
* Go to a specific index in the queue.
*
* @param index The index to go to. Should be in the queue.
*/
fun goto(index: Int)
/**
* Add songs to the currently playing item in the queue.
*
* @param songs The songs to add.
* @param ack The [StateAck] to return to [PlaybackStateManager].
*/
fun playNext(songs: List<Song>, ack: StateAck.PlayNext)
/**
* Add songs to the end of the queue.
*
* @param songs The songs to add.
* @param ack The [StateAck] to return to [PlaybackStateManager].
*/
fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue)
/**
* Move a song in the queue to a new position.
*
* @param from The index of the song to move.
* @param to The index to move the song to.
* @param ack The [StateAck] to return to [PlaybackStateManager].
*/
fun move(from: Int, to: Int, ack: StateAck.Move)
/**
* Remove a song from the queue.
*
* @param at The index of the song to remove.
* @param ack The [StateAck] to return to [PlaybackStateManager].
* @return The [Song] that was removed.
*/
fun remove(at: Int, ack: StateAck.Remove)
/**
* Reorder the queue.
*
* @param shuffled Whether the queue should be shuffled.
*/
fun shuffled(shuffled: Boolean)
/**
* Handle a deferred playback action.
*
* @param action The action to handle.
* @return Whether the action could be handled, or if it should be deferred for later.
*/
fun handleDeferred(action: DeferredPlayback): Boolean
/**
* Override the current held state with a saved state.
*
* @param parent The parent to play from.
* @param rawQueue The queue to use.
* @param ack The [StateAck] to return to [PlaybackStateManager]. If null, do not return any
* ack.
*/
fun applySavedState(parent: MusicParent?, rawQueue: RawQueue, ack: StateAck.NewPlayback?)
}
/**
* An acknowledgement that the state of the [PlaybackStateHolder] has changed. This is sent back to
* [PlaybackStateManager] once an operation in [PlaybackStateHolder] has completed so that the new
* state can be mirrored to the rest of the application.
*
* @author Alexander Capehart (OxygenCobalt)
*/
sealed interface StateAck {
/**
* @see PlaybackStateHolder.next
* @see PlaybackStateHolder.prev
* @see PlaybackStateHolder.goto
*/
data object IndexMoved : StateAck
/** @see PlaybackStateHolder.playNext */
data class PlayNext(val at: Int, val size: Int) : StateAck
/** @see PlaybackStateHolder.addToQueue */
data class AddToQueue(val at: Int, val size: Int) : StateAck
/** @see PlaybackStateHolder.move */
data class Move(val from: Int, val to: Int) : StateAck
/** @see PlaybackStateHolder.remove */
data class Remove(val index: Int) : StateAck
/** @see PlaybackStateHolder.shuffled */
data object QueueReordered : StateAck
/**
* @see PlaybackStateHolder.newPlayback
* @see PlaybackStateHolder.applySavedState
*/
data object NewPlayback : StateAck
/**
* @see PlaybackStateHolder.playing
* @see PlaybackStateHolder.seekTo
*/
data object ProgressionChanged : StateAck
/** @see PlaybackStateHolder.repeatMode */
data object RepeatModeChanged : StateAck
}
/**
* The queue as it is represented in the audio player held by [PlaybackStateHolder]. This should not
* be used as anything but a container. Use the provided fields to obtain saner queue information.
*
* @param heap The ordered list of all [Song]s in the queue.
* @param shuffledMapping A list of indices that remap the songs in [heap] into a shuffled queue.
* Empty if the queue is not shuffled.
* @param heapIndex The index of the current song in [heap]. Note that if shuffled, this will be a
* nonsensical value that cannot be used to obtain next and last songs without first resolving the
* queue.
*/
data class RawQueue(
val heap: List<Song>,
val shuffledMapping: List<Int>,
val heapIndex: Int,
) {
/** Whether the queue is currently shuffled. */
val isShuffled = shuffledMapping.isNotEmpty()
/**
* Resolve and return the exact [Song] sequence in the queue.
*
* @return The [Song]s in the queue, in order.
*/
fun resolveSongs() =
if (isShuffled) {
shuffledMapping.map { heap[it] }
} else {
heap
}
/**
* Resolve and return the current index of the queue.
*
* @return The current index of the queue.
*/
fun resolveIndex() =
if (isShuffled) {
shuffledMapping.indexOf(heapIndex)
} else {
heapIndex
}
companion object {
/** Create a blank instance. */
fun nil() = RawQueue(emptyList(), emptyList(), -1)
}
}
/**
* Represents the possible changes that can occur during certain queue mutation events.
*
* @param type The [Type] of the change to the internal queue state.
* @param instructions The update done to the resolved queue list.
*/
data class QueueChange(val type: Type, val instructions: UpdateInstructions) {
enum class Type {
/** Only the mapping has changed. */
MAPPING,
/** The mapping has changed, and the index also changed to align with it. */
INDEX,
/**
* The current song has changed, possibly alongside the mapping and index depending on the
* context.
*/
SONG
}
}
/** Possible long-running background tasks handled by the background playback task. */
sealed interface DeferredPlayback {
/** Restore the previously saved playback state. */
data object RestoreState : DeferredPlayback
/**
* Start shuffled playback of the entire music library. Analogous to the "Shuffle All" shortcut.
*/
data object ShuffleAll : DeferredPlayback
/**
* Start playing an audio file at the given [Uri].
*
* @param uri The [Uri] of the audio file to start playing.
*/
data class Open(val uri: Uri) : DeferredPlayback
}
/** A representation of the current state of audio playback. Use [from] to create an instance. */
class Progression
private constructor(
/** Whether the player is actively playing audio or set to play audio in the future. */
val isPlaying: Boolean,
/** Whether the player is actively playing audio in this moment. */
private val isAdvancing: Boolean,
/** The position when this instance was created, in milliseconds. */
private val initPositionMs: Long,
/** The time this instance was created, as a unix epoch timestamp. */
private val creationTime: Long
) {
/**
* Calculate the "real" playback position this instance contains, in milliseconds.
*
* @return If paused, the original position will be returned. Otherwise, it will be the original
* position plus the time elapsed since this state was created.
*/
fun calculateElapsedPositionMs() =
if (isAdvancing) {
initPositionMs + (SystemClock.elapsedRealtime() - creationTime)
} else {
// Not advancing due to buffering or some unrelated pausing, such as
// a transient audio focus change.
initPositionMs
}
/**
* Load this instance into a [PlaybackStateCompat].
*
* @param builder The [PlaybackStateCompat.Builder] to mutate.
* @return The same [PlaybackStateCompat.Builder] for easy chaining.
*/
fun intoPlaybackState(builder: PlaybackStateCompat.Builder): PlaybackStateCompat.Builder =
builder.setState(
// State represents the user's preference, not the actual player state.
// Doing this produces a better experience in the media control UI.
if (isPlaying) {
PlaybackStateCompat.STATE_PLAYING
} else {
PlaybackStateCompat.STATE_PAUSED
},
initPositionMs,
if (isAdvancing) {
1f
} else {
// Not advancing, so don't move the position.
0f
},
creationTime)
// Equality ignores the creation time to prevent functionally identical states
// from being non-equal.
override fun equals(other: Any?) =
other is Progression &&
isPlaying == other.isPlaying &&
isAdvancing == other.isAdvancing &&
initPositionMs == other.initPositionMs
override fun hashCode(): Int {
var result = isPlaying.hashCode()
result = 31 * result + isAdvancing.hashCode()
result = 31 * result + initPositionMs.hashCode()
return result
}
companion object {
/**
* Create a new instance.
*
* @param isPlaying Whether the player is actively playing audio or set to play audio in the
* future.
* @param isAdvancing Whether the player is actively playing audio in this moment.
* @param positionMs The current position of the player.
*/
fun from(isPlaying: Boolean, isAdvancing: Boolean, positionMs: Long) =
Progression(
isPlaying,
// Minor sanity check: Make sure that advancing can't occur if already paused.
isPlaying && isAdvancing,
positionMs,
SystemClock.elapsedRealtime())
fun nil() =
Progression(
isPlaying = false,
isAdvancing = false,
initPositionMs = 0,
creationTime = SystemClock.elapsedRealtime())
}
}

View file

@ -20,10 +20,10 @@ package org.oxycblt.auxio.playback.state
import javax.inject.Inject
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.list.adapter.UpdateInstructions
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.MutableQueue
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
@ -40,19 +40,32 @@ import org.oxycblt.auxio.util.logW
* PlaybackService.
*
* Internal consumers should usually use [Listener], however the component that manages the player
* itself should instead use [InternalPlayer].
* itself should instead use [PlaybackStateHolder].
*
* @author Alexander Capehart (OxygenCobalt)
*/
interface PlaybackStateManager {
/** The current [Queue]. */
val queue: Queue
/** The [MusicParent] currently being played. Null if playback is occurring from all songs. */
/** The current [Progression] of the audio player */
val progression: Progression
/** The current [RepeatMode]. */
val repeatMode: RepeatMode
/** The current [MusicParent] being played from */
val parent: MusicParent?
/** The current [InternalPlayer] state. */
val playerState: InternalPlayer.State
/** The current [RepeatMode] */
var repeatMode: RepeatMode
/** The current [Song] being played. Null if nothing is playing. */
val currentSong: Song?
/** The current queue of [Song]s. */
val queue: List<Song>
/** The index of the currently playing [Song] in the queue. */
val index: Int
/** Whether the queue is shuffled or not. */
val isShuffled: Boolean
/** The audio session ID of the internal player. Null if no internal player exists. */
val currentAudioSessionId: Int?
@ -75,23 +88,25 @@ interface PlaybackStateManager {
fun removeListener(listener: Listener)
/**
* Register an [InternalPlayer] for this instance. This instance will handle translating the
* current playback state into audio playback. There can be only one [InternalPlayer] at a time.
* Will invoke [InternalPlayer] methods to initialize the instance with the current state.
* Register an [PlaybackStateHolder] for this instance. This instance will handle translating
* the current playback state into audio playback. There can be only one [PlaybackStateHolder]
* at a time. Will invoke [PlaybackStateHolder] methods to initialize the instance with the
* current state.
*
* @param internalPlayer The [InternalPlayer] to register. Will do nothing if already
* @param stateHolder The [PlaybackStateHolder] to register. Will do nothing if already
* registered.
*/
fun registerInternalPlayer(internalPlayer: InternalPlayer)
fun registerStateHolder(stateHolder: PlaybackStateHolder)
/**
* Unregister the [InternalPlayer] from this instance, prevent it from receiving any further
* commands.
* Unregister the [PlaybackStateHolder] from this instance, prevent it from receiving any
* further commands.
*
* @param internalPlayer The [InternalPlayer] to unregister. Must be the current
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
* @param stateHolder The [PlaybackStateHolder] to unregister. Must be the current
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
* implementation.
*/
fun unregisterInternalPlayer(internalPlayer: InternalPlayer)
fun unregisterStateHolder(stateHolder: PlaybackStateHolder)
/**
* Start new playback.
@ -171,38 +186,49 @@ interface PlaybackStateManager {
*
* @param shuffled Whether to shuffle the queue or not.
*/
fun reorder(shuffled: Boolean)
fun shuffled(shuffled: Boolean)
/**
* Synchronize the state of this instance with the current [InternalPlayer].
* Acknowledges that an event has happened that modified the state held by the current
* [PlaybackStateHolder].
*
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
* @param stateHolder The [PlaybackStateHolder] to synchronize with. Must be the current
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
* implementation.
* @param ack The [StateAck] to acknowledge.
*/
fun synchronizeState(internalPlayer: InternalPlayer)
fun ack(stateHolder: PlaybackStateHolder, ack: StateAck)
/**
* Start a [InternalPlayer.Action] for the current [InternalPlayer] to handle eventually.
* Start a [DeferredPlayback] for the current [PlaybackStateHolder] to handle eventually.
*
* @param action The [InternalPlayer.Action] to perform.
* @param action The [DeferredPlayback] to perform.
*/
fun startAction(action: InternalPlayer.Action)
fun playDeferred(action: DeferredPlayback)
/**
* Request that the pending [InternalPlayer.Action] (if any) be passed to the given
* [InternalPlayer].
* Request that the pending [DeferredPlayback] (if any) be passed to the given
* [PlaybackStateHolder].
*
* @param internalPlayer The [InternalPlayer] to synchronize with. Must be the current
* [InternalPlayer]. Does nothing if invoked by another [InternalPlayer] implementation.
* @param stateHolder The [PlaybackStateHolder] to synchronize with. Must be the current
* [PlaybackStateHolder]. Does nothing if invoked by another [PlaybackStateHolder]
* implementation.
*/
fun requestAction(internalPlayer: InternalPlayer)
fun requestAction(stateHolder: PlaybackStateHolder)
/**
* Update whether playback is ongoing or not.
*
* @param isPlaying Whether playback is ongoing or not.
*/
fun setPlaying(isPlaying: Boolean)
fun playing(isPlaying: Boolean)
/**
* Update the current [RepeatMode].
*
* @param repeatMode The new [RepeatMode].
*/
fun repeatMode(repeatMode: RepeatMode)
/**
* Seek to the given position in the currently playing [Song].
@ -240,100 +266,140 @@ interface PlaybackStateManager {
* Called when the position of the currently playing item has changed, changing the current
* [Song], but no other queue attribute has changed.
*
* @param queue The new [Queue].
* @param index The new index of the currently playing [Song].
*/
fun onIndexMoved(queue: Queue) {}
fun onIndexMoved(index: Int) {}
/**
* Called when the [Queue] changed in a manner outlined by the given [Queue.Change].
* Called when the queue changed in a manner outlined by the given [DeferredPlayback].
*
* @param queue The new [Queue].
* @param change The type of [Queue.Change] that occurred.
* @param queue The songs of the new queue.
* @param index The new index of the currently playing [Song].
* @param change The [QueueChange] that occurred.
*/
fun onQueueChanged(queue: Queue, change: Queue.Change) {}
fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {}
/**
* Called when the [Queue] has changed in a non-trivial manner (such as re-shuffling), but
* the currently playing [Song] has not.
* Called when the queue has changed in a non-trivial manner (such as re-shuffling), but the
* currently playing [Song] has not.
*
* @param queue The new [Queue].
* @param queue The songs of the new queue.
* @param index The new index of the currently playing [Song].
* @param isShuffled Whether the queue is shuffled or not.
*/
fun onQueueReordered(queue: Queue) {}
fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {}
/**
* Called when a new playback configuration was created.
*
* @param queue The new [Queue].
* @param parent The new [MusicParent] being played from, or null if playing from all songs.
* @param parent The [MusicParent] item currently being played from.
* @param queue The queue of [Song]s to play from.
* @param index The index of the currently playing [Song].
* @param isShuffled Whether the queue is shuffled or not.
*/
fun onNewPlayback(queue: Queue, parent: MusicParent?) {}
fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {}
/**
* Called when the state of the [InternalPlayer] changes.
* Called when the state of the audio player changes.
*
* @param state The new state of the [InternalPlayer].
* @param progression The new state of the audio player.
*/
fun onStateChanged(state: InternalPlayer.State) {}
fun onProgressionChanged(progression: Progression) {}
/**
* Called when the [RepeatMode] changes.
*
* @param repeatMode The new [RepeatMode].
*/
fun onRepeatChanged(repeatMode: RepeatMode) {}
fun onRepeatModeChanged(repeatMode: RepeatMode) {}
}
/**
* A condensed representation of the playback state that can be persisted.
*
* @param parent The [MusicParent] item currently being played from.
* @param queueState The [Queue.SavedState]
* @param positionMs The current position in the currently played song, in ms
* @param repeatMode The current [RepeatMode].
*/
data class SavedState(
val parent: MusicParent?,
val queueState: Queue.SavedState,
val positionMs: Long,
val repeatMode: RepeatMode,
val parent: MusicParent?,
val heap: List<Song?>,
val shuffledMapping: List<Int>,
val index: Int,
val songUid: Music.UID,
)
}
class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
private data class StateMirror(
val progression: Progression,
val repeatMode: RepeatMode,
val parent: MusicParent?,
val queue: List<Song>,
val index: Int,
val isShuffled: Boolean,
val rawQueue: RawQueue
)
private val listeners = mutableListOf<PlaybackStateManager.Listener>()
@Volatile private var internalPlayer: InternalPlayer? = null
@Volatile private var pendingAction: InternalPlayer.Action? = null
@Volatile
private var stateMirror =
StateMirror(
progression = Progression.nil(),
repeatMode = RepeatMode.NONE,
parent = null,
queue = emptyList(),
index = -1,
isShuffled = false,
rawQueue = RawQueue.nil())
@Volatile private var stateHolder: PlaybackStateHolder? = null
@Volatile private var pendingDeferredPlayback: DeferredPlayback? = null
@Volatile private var isInitialized = false
override val queue = MutableQueue()
@Volatile
override var parent: MusicParent? = null
private set
override val progression
get() = stateMirror.progression
@Volatile
override var playerState = InternalPlayer.State.from(isPlaying = false, isAdvancing = false, 0)
private set
override val repeatMode
get() = stateMirror.repeatMode
@Volatile
override var repeatMode = RepeatMode.NONE
set(value) {
field = value
notifyRepeatModeChanged()
}
override val parent
get() = stateMirror.parent
override val currentSong
get() = stateMirror.queue.getOrNull(stateMirror.index)
override val queue
get() = stateMirror.queue
override val index
get() = stateMirror.index
override val isShuffled
get() = stateMirror.isShuffled
override val currentAudioSessionId: Int?
get() = internalPlayer?.audioSessionId
get() = stateHolder?.audioSessionId
@Synchronized
override fun addListener(listener: PlaybackStateManager.Listener) {
logD("Adding $listener to listeners")
if (isInitialized) {
listener.onNewPlayback(queue, parent)
listener.onRepeatChanged(repeatMode)
listener.onStateChanged(playerState)
}
listeners.add(listener)
if (isInitialized) {
logD("Sending initial state to $listener")
listener.onNewPlayback(
stateMirror.parent, stateMirror.queue, stateMirror.index, stateMirror.isShuffled)
listener.onProgressionChanged(stateMirror.progression)
listener.onRepeatModeChanged(stateMirror.repeatMode)
}
}
@Synchronized
@ -345,208 +411,301 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
}
@Synchronized
override fun registerInternalPlayer(internalPlayer: InternalPlayer) {
if (this.internalPlayer != null) {
override fun registerStateHolder(stateHolder: PlaybackStateHolder) {
if (this.stateHolder != null) {
logW("Internal player is already registered")
return
}
logD("Registering internal player $internalPlayer")
if (isInitialized) {
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
internalPlayer.seekTo(playerState.calculateElapsedPositionMs())
// See if there's any action that has been queued.
requestAction(internalPlayer)
// Once initialized, try to synchronize with the player state it has created.
synchronizeState(internalPlayer)
this.stateHolder = stateHolder
if (isInitialized && stateMirror.index > -1) {
stateHolder.applySavedState(stateMirror.parent, stateMirror.rawQueue, null)
stateHolder.seekTo(stateMirror.progression.calculateElapsedPositionMs())
stateHolder.playing(false)
pendingDeferredPlayback?.let(stateHolder::handleDeferred)
}
this.internalPlayer = internalPlayer
}
@Synchronized
override fun unregisterInternalPlayer(internalPlayer: InternalPlayer) {
if (this.internalPlayer !== internalPlayer) {
override fun unregisterStateHolder(stateHolder: PlaybackStateHolder) {
if (this.stateHolder !== stateHolder) {
logW("Given internal player did not match current internal player")
return
}
logD("Unregistering internal player $internalPlayer")
logD("Unregistering internal player $stateHolder")
this.internalPlayer = null
this.stateHolder = null
}
// --- PLAYING FUNCTIONS ---
@Synchronized
override fun play(song: Song?, parent: MusicParent?, queue: List<Song>, shuffled: Boolean) {
val internalPlayer = internalPlayer ?: return
val stateHolder = stateHolder ?: return
logD("Playing $song from $parent in ${queue.size}-song queue [shuffled=$shuffled]")
// Set up parent and queue
this.parent = parent
this.queue.start(song, queue, shuffled)
// Notify components of changes
notifyNewPlayback()
internalPlayer.loadSong(this.queue.currentSong, true)
// Played something, so we are initialized now
isInitialized = true
stateHolder.newPlayback(queue, song, parent, shuffled)
}
// --- QUEUE FUNCTIONS ---
@Synchronized
override fun next() {
val internalPlayer = internalPlayer ?: return
var play = true
if (!queue.goto(queue.index + 1)) {
queue.goto(0)
play = repeatMode == RepeatMode.ALL
logD("At end of queue, wrapping around to position 0 [play=$play]")
} else {
logD("Moving to next song")
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, play)
val stateHolder = stateHolder ?: return
logD("Going to next song")
stateHolder.next()
}
@Synchronized
override fun prev() {
val internalPlayer = internalPlayer ?: return
// If enabled, rewind before skipping back if the position is past 3 seconds [3000ms]
if (internalPlayer.shouldRewindWithPrev) {
logD("Rewinding current song")
rewind()
setPlaying(true)
} else {
logD("Moving to previous song")
if (!queue.goto(queue.index - 1)) {
queue.goto(0)
}
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
}
val stateHolder = stateHolder ?: return
logD("Going to previous song")
stateHolder.prev()
}
@Synchronized
override fun goto(index: Int) {
val internalPlayer = internalPlayer ?: return
if (queue.goto(index)) {
logD("Moving to $index")
notifyIndexMoved()
internalPlayer.loadSong(queue.currentSong, true)
} else {
logW("$index was not in bounds, could not move to it")
}
val stateHolder = stateHolder ?: return
logD("Going to index $index")
stateHolder.goto(index)
}
@Synchronized
override fun playNext(songs: List<Song>) {
if (queue.currentSong == null) {
if (currentSong == null) {
logD("Nothing playing, short-circuiting to new playback")
play(songs[0], null, songs, false)
} else {
val stateHolder = stateHolder ?: return
logD("Adding ${songs.size} songs to start of queue")
notifyQueueChanged(queue.addToTop(songs))
stateHolder.playNext(songs, StateAck.PlayNext(stateMirror.index + 1, songs.size))
}
}
@Synchronized
override fun addToQueue(songs: List<Song>) {
if (queue.currentSong == null) {
if (currentSong == null) {
logD("Nothing playing, short-circuiting to new playback")
play(songs[0], null, songs, false)
} else {
val stateHolder = stateHolder ?: return
logD("Adding ${songs.size} songs to end of queue")
notifyQueueChanged(queue.addToBottom(songs))
stateHolder.addToQueue(songs, StateAck.AddToQueue(stateMirror.index + 1, songs.size))
}
}
@Synchronized
override fun moveQueueItem(src: Int, dst: Int) {
val stateHolder = stateHolder ?: return
logD("Moving item $src to position $dst")
notifyQueueChanged(queue.move(src, dst))
stateHolder.move(src, dst, StateAck.Move(src, dst))
}
@Synchronized
override fun removeQueueItem(at: Int) {
val internalPlayer = internalPlayer ?: return
val stateHolder = stateHolder ?: return
logD("Removing item at $at")
val change = queue.remove(at)
if (change.type == Queue.Change.Type.SONG) {
internalPlayer.loadSong(queue.currentSong, playerState.isPlaying)
}
notifyQueueChanged(change)
stateHolder.remove(at, StateAck.Remove(at))
}
@Synchronized
override fun reorder(shuffled: Boolean) {
override fun shuffled(shuffled: Boolean) {
val stateHolder = stateHolder ?: return
logD("Reordering queue [shuffled=$shuffled]")
queue.reorder(shuffled)
notifyQueueReordered()
stateHolder.shuffled(shuffled)
}
// --- INTERNAL PLAYER FUNCTIONS ---
@Synchronized
override fun synchronizeState(internalPlayer: InternalPlayer) {
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
logW("Given internal player did not match current internal player")
return
}
val newState = internalPlayer.getState(queue.currentSong?.durationMs ?: 0)
if (newState != playerState) {
playerState = newState
notifyStateChanged()
}
}
@Synchronized
override fun startAction(action: InternalPlayer.Action) {
val internalPlayer = internalPlayer
if (internalPlayer == null || !internalPlayer.performAction(action)) {
override fun playDeferred(action: DeferredPlayback) {
val stateHolder = stateHolder
if (stateHolder == null || !stateHolder.handleDeferred(action)) {
logD("Internal player not present or did not consume action, waiting")
pendingAction = action
pendingDeferredPlayback = action
}
}
@Synchronized
override fun requestAction(internalPlayer: InternalPlayer) {
if (BuildConfig.DEBUG && this.internalPlayer !== internalPlayer) {
override fun requestAction(stateHolder: PlaybackStateHolder) {
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
logW("Given internal player did not match current internal player")
return
}
if (pendingAction?.let(internalPlayer::performAction) == true) {
if (pendingDeferredPlayback?.let(stateHolder::handleDeferred) == true) {
logD("Pending action consumed")
pendingAction = null
pendingDeferredPlayback = null
}
}
@Synchronized
override fun setPlaying(isPlaying: Boolean) {
override fun playing(isPlaying: Boolean) {
val stateHolder = stateHolder ?: return
logD("Updating playing state to $isPlaying")
internalPlayer?.setPlaying(isPlaying)
stateHolder.playing(isPlaying)
}
@Synchronized
override fun repeatMode(repeatMode: RepeatMode) {
val stateHolder = stateHolder ?: return
logD("Updating repeat mode to $repeatMode")
stateHolder.repeatMode(repeatMode)
}
@Synchronized
override fun seekTo(positionMs: Long) {
val stateHolder = stateHolder ?: return
logD("Seeking to ${positionMs}ms")
internalPlayer?.seekTo(positionMs)
stateHolder.seekTo(positionMs)
}
@Synchronized
override fun ack(stateHolder: PlaybackStateHolder, ack: StateAck) {
if (BuildConfig.DEBUG && this.stateHolder !== stateHolder) {
logW("Given internal player did not match current internal player")
return
}
when (ack) {
is StateAck.IndexMoved -> {
val rawQueue = stateHolder.resolveQueue()
stateMirror = stateMirror.copy(index = rawQueue.resolveIndex(), rawQueue = rawQueue)
listeners.forEach { it.onIndexMoved(stateMirror.index) }
}
is StateAck.PlayNext -> {
val rawQueue = stateHolder.resolveQueue()
val change =
QueueChange(QueueChange.Type.MAPPING, UpdateInstructions.Add(ack.at, ack.size))
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
rawQueue = rawQueue,
)
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateAck.AddToQueue -> {
val rawQueue = stateHolder.resolveQueue()
val change =
QueueChange(QueueChange.Type.MAPPING, UpdateInstructions.Add(ack.at, ack.size))
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
rawQueue = rawQueue,
)
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateAck.Move -> {
val rawQueue = stateHolder.resolveQueue()
val newIndex = rawQueue.resolveIndex()
val change =
QueueChange(
if (stateMirror.index != newIndex) QueueChange.Type.INDEX
else QueueChange.Type.MAPPING,
UpdateInstructions.Move(ack.from, ack.to))
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
index = newIndex,
rawQueue = rawQueue,
)
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateAck.Remove -> {
val rawQueue = stateHolder.resolveQueue()
val newIndex = rawQueue.resolveIndex()
val change =
QueueChange(
when {
ack.index == stateMirror.index -> QueueChange.Type.SONG
stateMirror.index != newIndex -> QueueChange.Type.INDEX
else -> QueueChange.Type.MAPPING
},
UpdateInstructions.Remove(ack.index, 1))
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
index = newIndex,
rawQueue = rawQueue,
)
listeners.forEach {
it.onQueueChanged(stateMirror.queue, stateMirror.index, change)
}
}
is StateAck.QueueReordered -> {
val rawQueue = stateHolder.resolveQueue()
stateMirror =
stateMirror.copy(
queue = rawQueue.resolveSongs(),
index = rawQueue.resolveIndex(),
isShuffled = rawQueue.isShuffled,
rawQueue = rawQueue)
listeners.forEach {
it.onQueueReordered(
stateMirror.queue, stateMirror.index, stateMirror.isShuffled)
}
}
is StateAck.NewPlayback -> {
val rawQueue = stateHolder.resolveQueue()
stateMirror =
stateMirror.copy(
parent = stateHolder.parent,
queue = rawQueue.resolveSongs(),
index = rawQueue.resolveIndex(),
isShuffled = rawQueue.isShuffled,
rawQueue = rawQueue)
listeners.forEach {
it.onNewPlayback(
stateMirror.parent,
stateMirror.queue,
stateMirror.index,
stateMirror.isShuffled)
}
}
is StateAck.ProgressionChanged -> {
stateMirror =
stateMirror.copy(
progression = stateHolder.progression,
)
listeners.forEach { it.onProgressionChanged(stateMirror.progression) }
}
is StateAck.RepeatModeChanged -> {
stateMirror =
stateMirror.copy(
repeatMode = stateHolder.repeatMode,
)
listeners.forEach { it.onRepeatModeChanged(stateMirror.repeatMode) }
}
}
}
// --- PERSISTENCE FUNCTIONS ---
@Synchronized
override fun toSavedState() =
queue.toSavedState()?.let {
PlaybackStateManager.SavedState(
parent = parent,
queueState = it,
positionMs = playerState.calculateElapsedPositionMs(),
repeatMode = repeatMode)
}
override fun toSavedState(): PlaybackStateManager.SavedState? {
val currentSong = currentSong ?: return null
return PlaybackStateManager.SavedState(
positionMs = stateMirror.progression.calculateElapsedPositionMs(),
repeatMode = stateMirror.repeatMode,
parent = stateMirror.parent,
heap = stateMirror.rawQueue.heap,
shuffledMapping = stateMirror.rawQueue.shuffledMapping,
index = stateMirror.index,
songUid = currentSong.uid,
)
}
@Synchronized
override fun applySavedState(
@ -554,77 +713,84 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
destructive: Boolean
) {
if (isInitialized && !destructive) {
logW("Already initialized, cannot apply saved state")
logW("Already initialized, cannot apply saved state")
return
}
val internalPlayer = internalPlayer ?: return
logD("Applying state $savedState")
val lastSong = queue.currentSong
parent = savedState.parent
queue.applySavedState(savedState.queueState)
repeatMode = savedState.repeatMode
notifyNewPlayback()
// Check if we need to reload the player with a new music file, or if we can just leave
// it be. Specifically done so we don't pause on music updates that don't really change
// what's playing (ex. playlist editing)
if (lastSong != queue.currentSong) {
logD("Song changed, must reload player")
// Continuing playback while also possibly doing drastic state updates is
// a bad idea, so pause.
internalPlayer.loadSong(queue.currentSong, false)
if (queue.currentSong != null) {
logD("Seeking to saved position ${savedState.positionMs}ms")
// Internal player may have reloaded the media item, re-seek to the previous
// position
seekTo(savedState.positionMs)
// The heap may not be the same if the song composition changed between state saves/reloads.
// This also means that we must modify the shuffled mapping as well, in what it points to
// and it's general composition.
val heap = mutableListOf<Song>()
val adjustments = mutableListOf<Int?>()
var currentShift = 0
for (song in savedState.heap) {
if (song != null) {
heap.add(song)
adjustments.add(currentShift)
} else {
adjustments.add(null)
currentShift -= 1
}
}
logD("Created adjustment mapping [max shift=$currentShift]")
val shuffledMapping =
savedState.shuffledMapping.mapNotNullTo(mutableListOf()) { index ->
adjustments[index]?.let { index + it }
}
// Make sure we re-align the index to point to the previously playing song.
fun pointingAtSong(): Boolean {
val currentSong =
if (shuffledMapping.isNotEmpty()) {
shuffledMapping.getOrNull(savedState.index)?.let { heap.getOrNull(it) }
} else {
heap.getOrNull(savedState.index)
}
return currentSong?.uid == savedState.songUid
}
var index = savedState.index
while (!pointingAtSong() && index > -1) {
index--
}
logD("Corrected index: ${savedState.index} -> $index")
check(shuffledMapping.all { it in heap.indices }) {
"Queue inconsistency detected: Shuffled mapping indices out of heap bounds"
}
val rawQueue =
RawQueue(
heap = heap,
shuffledMapping = shuffledMapping,
heapIndex =
if (shuffledMapping.isNotEmpty()) {
shuffledMapping[savedState.index]
} else {
index
})
if (index > -1) {
// Valid state where something needs to be played, direct the stateholder to apply
// this new state.
val oldStateMirror = stateMirror
if (oldStateMirror.rawQueue != rawQueue) {
logD("Queue changed, must reload player")
stateHolder?.applySavedState(parent, rawQueue, StateAck.NewPlayback)
stateHolder?.playing(false)
}
if (oldStateMirror.progression.calculateElapsedPositionMs() != savedState.positionMs) {
logD("Seeking to saved position ${savedState.positionMs}ms")
stateHolder?.seekTo(savedState.positionMs)
stateHolder?.playing(false)
}
}
isInitialized = true
}
// --- CALLBACKS ---
private fun notifyIndexMoved() {
logD("Dispatching index change")
for (callback in listeners) {
callback.onIndexMoved(queue)
}
}
private fun notifyQueueChanged(change: Queue.Change) {
logD("Dispatching queue change $change")
for (callback in listeners) {
callback.onQueueChanged(queue, change)
}
}
private fun notifyQueueReordered() {
logD("Dispatching queue reordering")
for (callback in listeners) {
callback.onQueueReordered(queue)
}
}
private fun notifyNewPlayback() {
logD("Dispatching new playback")
for (callback in listeners) {
callback.onNewPlayback(queue, parent)
}
}
private fun notifyStateChanged() {
logD("Dispatching player state change")
for (callback in listeners) {
callback.onStateChanged(playerState)
}
}
private fun notifyRepeatModeChanged() {
logD("Dispatching repeat mode change")
for (callback in listeners) {
callback.onRepeatChanged(repeatMode)
}
}
}

View file

@ -29,18 +29,10 @@ import java.util.*
*
* @author media3 team, Alexander Capehart (OxygenCobalt)
*/
class BetterShuffleOrder
private constructor(private val shuffled: IntArray, private val random: Random) : ShuffleOrder {
class BetterShuffleOrder(private val shuffled: IntArray) : ShuffleOrder {
private val indexInShuffled: IntArray = IntArray(shuffled.size)
/**
* Creates an instance with a specified length.
*
* @param length The length of the shuffle order.
*/
constructor(length: Int) : this(length, Random())
constructor(length: Int, random: Random) : this(createShuffledList(length, random), random)
constructor(length: Int, startIndex: Int) : this(createShuffledList(length, startIndex))
init {
for (i in shuffled.indices) {
@ -70,7 +62,12 @@ private constructor(private val shuffled: IntArray, private val random: Random)
return if (shuffled.isNotEmpty()) shuffled[0] else C.INDEX_UNSET
}
@Suppress("KotlinConstantConditions") // Bugged for this function
override fun cloneAndInsert(insertionIndex: Int, insertionCount: Int): ShuffleOrder {
if (shuffled.isEmpty()) {
return BetterShuffleOrder(insertionCount, -1)
}
val newShuffled = IntArray(shuffled.size + insertionCount)
val pivot = indexInShuffled[insertionIndex]
for (i in shuffled.indices) {
@ -88,7 +85,7 @@ private constructor(private val shuffled: IntArray, private val random: Random)
for (i in 0 until insertionCount) {
newShuffled[pivot + i + 1] = insertionIndex + i + 1
}
return BetterShuffleOrder(newShuffled, Random(random.nextLong()))
return BetterShuffleOrder(newShuffled)
}
override fun cloneAndRemove(indexFrom: Int, indexToExclusive: Int): ShuffleOrder {
@ -104,21 +101,27 @@ private constructor(private val shuffled: IntArray, private val random: Random)
else shuffled[i]
}
}
return BetterShuffleOrder(newShuffled, Random(random.nextLong()))
return BetterShuffleOrder(newShuffled)
}
override fun cloneAndClear(): ShuffleOrder {
return BetterShuffleOrder(0, Random(random.nextLong()))
return BetterShuffleOrder(0, -1)
}
companion object {
private fun createShuffledList(length: Int, random: Random): IntArray {
private fun createShuffledList(length: Int, startIndex: Int): IntArray {
val shuffled = IntArray(length)
for (i in 0 until length) {
val swapIndex = random.nextInt(i + 1)
val swapIndex = (0..i).random()
shuffled[i] = shuffled[swapIndex]
shuffled[swapIndex] = i
}
if (startIndex != -1) {
val startIndexInShuffled = shuffled.indexOf(startIndex)
val temp = shuffled[0]
shuffled[0] = shuffled[startIndexInShuffled]
shuffled[startIndexInShuffled] = temp
}
return shuffled
}
}

View file

@ -39,7 +39,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
// TODO: Figure this out
override fun onReceive(context: Context, intent: Intent) {
if (playbackManager.queue.currentSong != null) {
if (playbackManager.currentSong != null) {
// We have a song, so we can assume that the service will start a foreground state.
// At least, I hope. Again, *this is why we don't do this*. I cannot describe how
// stupid this is with the state of foreground services on modern android. One

View file

@ -38,9 +38,9 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.resolveNames
import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.util.logD
@ -117,28 +117,29 @@ constructor(
// --- PLAYBACKSTATEMANAGER OVERRIDES ---
override fun onIndexMoved(queue: Queue) {
updateMediaMetadata(queue.currentSong, playbackManager.parent)
override fun onIndexMoved(index: Int) {
updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
invalidateSessionState()
}
override fun onQueueChanged(queue: Queue, change: Queue.Change) {
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
updateQueue(queue)
when (change.type) {
// Nothing special to do with mapping changes.
Queue.Change.Type.MAPPING -> {}
QueueChange.Type.MAPPING -> {}
// Index changed, ensure playback state's index changes.
Queue.Change.Type.INDEX -> invalidateSessionState()
QueueChange.Type.INDEX -> invalidateSessionState()
// Song changed, ensure metadata changes.
Queue.Change.Type.SONG -> updateMediaMetadata(queue.currentSong, playbackManager.parent)
QueueChange.Type.SONG ->
updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
}
}
override fun onQueueReordered(queue: Queue) {
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) {
updateQueue(queue)
invalidateSessionState()
mediaSession.setShuffleMode(
if (queue.isShuffled) {
if (isShuffled) {
PlaybackStateCompat.SHUFFLE_MODE_ALL
} else {
PlaybackStateCompat.SHUFFLE_MODE_NONE
@ -146,21 +147,26 @@ constructor(
invalidateSecondaryAction()
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) {
updateMediaMetadata(queue.currentSong, parent)
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) {
updateMediaMetadata(playbackManager.currentSong, parent)
updateQueue(queue)
invalidateSessionState()
}
override fun onStateChanged(state: InternalPlayer.State) {
override fun onProgressionChanged(progression: Progression) {
invalidateSessionState()
notification.updatePlaying(playbackManager.playerState.isPlaying)
notification.updatePlaying(playbackManager.progression.isPlaying)
if (!bitmapProvider.isBusy) {
listener?.onPostNotification(notification)
}
}
override fun onRepeatChanged(repeatMode: RepeatMode) {
override fun onRepeatModeChanged(repeatMode: RepeatMode) {
mediaSession.setRepeatMode(
when (repeatMode) {
RepeatMode.NONE -> PlaybackStateCompat.REPEAT_MODE_NONE
@ -175,7 +181,7 @@ constructor(
override fun onImageSettingsChanged() {
// Need to reload the metadata cover.
updateMediaMetadata(playbackManager.queue.currentSong, playbackManager.parent)
updateMediaMetadata(playbackManager.currentSong, playbackManager.parent)
}
override fun onNotificationActionChanged() {
@ -211,11 +217,11 @@ constructor(
}
override fun onPlay() {
playbackManager.setPlaying(true)
playbackManager.playing(true)
}
override fun onPause() {
playbackManager.setPlaying(false)
playbackManager.playing(false)
}
override fun onSkipToNext() {
@ -236,21 +242,21 @@ constructor(
override fun onRewind() {
playbackManager.rewind()
playbackManager.setPlaying(true)
playbackManager.playing(true)
}
override fun onSetRepeatMode(repeatMode: Int) {
playbackManager.repeatMode =
playbackManager.repeatMode(
when (repeatMode) {
PlaybackStateCompat.REPEAT_MODE_ALL -> RepeatMode.ALL
PlaybackStateCompat.REPEAT_MODE_GROUP -> RepeatMode.ALL
PlaybackStateCompat.REPEAT_MODE_ONE -> RepeatMode.TRACK
else -> RepeatMode.NONE
}
})
}
override fun onSetShuffleMode(shuffleMode: Int) {
playbackManager.reorder(
playbackManager.shuffled(
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_ALL ||
shuffleMode == PlaybackStateCompat.SHUFFLE_MODE_GROUP)
}
@ -354,9 +360,9 @@ constructor(
*
* @param queue The current queue to upload.
*/
private fun updateQueue(queue: Queue) {
private fun updateQueue(queue: List<Song>) {
val queueItems =
queue.resolve().mapIndexed { i, song ->
queue.mapIndexed { i, song ->
val description =
MediaDescriptionCompat.Builder()
// Media ID should not be the item index but rather the UID,
@ -383,11 +389,11 @@ constructor(
val state =
// InternalPlayer.State handles position/state information.
playbackManager.playerState
playbackManager.progression
.intoPlaybackState(PlaybackStateCompat.Builder())
.setActions(ACTIONS)
// Active queue ID corresponds to the indices we populated prior, use them here.
.setActiveQueueItemId(playbackManager.queue.index.toLong())
.setActiveQueueItemId(playbackManager.index.toLong())
// Android 13+ relies on custom actions in the notification.
@ -399,7 +405,7 @@ constructor(
PlaybackStateCompat.CustomAction.Builder(
PlaybackService.ACTION_INVERT_SHUFFLE,
context.getString(R.string.desc_shuffle),
if (playbackManager.queue.isShuffled) {
if (playbackManager.isShuffled) {
R.drawable.ic_shuffle_on_24
} else {
R.drawable.ic_shuffle_off_24
@ -435,7 +441,7 @@ constructor(
when (playbackSettings.notificationAction) {
ActionMode.SHUFFLE -> {
logD("Using shuffle notification action")
notification.updateShuffled(playbackManager.queue.isShuffled)
notification.updateShuffled(playbackManager.isShuffled)
}
else -> {
logD("Using repeat mode notification action")

View file

@ -44,18 +44,25 @@ import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.list.ListSettings
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.MusicRepository
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSettings
import org.oxycblt.auxio.playback.persist.PersistenceRepository
import org.oxycblt.auxio.playback.replaygain.ReplayGainAudioProcessor
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.DeferredPlayback
import org.oxycblt.auxio.playback.state.PlaybackStateHolder
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.RawQueue
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.state.StateAck
import org.oxycblt.auxio.service.ForegroundManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logE
@ -82,7 +89,8 @@ import org.oxycblt.auxio.widgets.WidgetProvider
class PlaybackService :
Service(),
Player.Listener,
InternalPlayer,
PlaybackStateHolder,
PlaybackSettings.Listener,
MediaSessionComponent.Listener,
MusicRepository.UpdateListener {
// Player components
@ -111,6 +119,7 @@ class PlaybackService :
private val serviceJob = Job()
private val restoreScope = CoroutineScope(serviceJob + Dispatchers.IO)
private val saveScope = CoroutineScope(serviceJob + Dispatchers.IO)
private var currentSaveJob: Job? = null
// --- SERVICE OVERRIDES ---
@ -148,9 +157,10 @@ class PlaybackService :
foregroundManager = ForegroundManager(this)
// Initialize any listener-dependent components last as we wouldn't want a listener race
// condition to cause us to load music before we were fully initialize.
playbackManager.registerInternalPlayer(this)
playbackManager.registerStateHolder(this)
musicRepository.addUpdateListener(this)
mediaSessionComponent.registerListener(this)
playbackSettings.registerListener(this)
val intentFilter =
IntentFilter().apply {
@ -181,7 +191,13 @@ class PlaybackService :
override fun onBind(intent: Intent): IBinder? = null
// TODO: Implement task removal (Have to radically alter state saving to occur at runtime)
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
if (!playbackManager.progression.isPlaying) {
playbackManager.playing(false)
endSession()
}
}
override fun onDestroy() {
super.onDestroy()
@ -189,9 +205,10 @@ class PlaybackService :
foregroundManager.release()
// Pause just in case this destruction was unexpected.
playbackManager.setPlaying(false)
playbackManager.unregisterInternalPlayer(this)
playbackManager.playing(false)
playbackManager.unregisterStateHolder(this)
musicRepository.removeUpdateListener(this)
playbackSettings.unregisterListener(this)
unregisterReceiver(systemReceiver)
serviceJob.cancel()
@ -210,101 +227,327 @@ class PlaybackService :
logD("Service destroyed")
}
// --- CONTROLLER OVERRIDES ---
// --- PLAYBACKSTATEHOLDER OVERRIDES ---
override val progression: Progression
get() =
player.currentMediaItem?.let {
Progression.from(
player.playWhenReady,
player.isPlaying,
// The position value can be below zero or past the expected duration, make
// sure we handle that.
player.currentPosition.coerceAtLeast(0).coerceAtMost(it.song.durationMs))
}
?: Progression.nil()
override val repeatMode
get() =
when (val repeatMode = player.repeatMode) {
Player.REPEAT_MODE_OFF -> RepeatMode.NONE
Player.REPEAT_MODE_ONE -> RepeatMode.TRACK
Player.REPEAT_MODE_ALL -> RepeatMode.ALL
else -> throw IllegalStateException("Unknown repeat mode: $repeatMode")
}
override var parent: MusicParent? = null
override fun resolveQueue(): RawQueue {
val heap = (0 until player.mediaItemCount).map { player.getMediaItemAt(it).song }
val shuffledMapping =
if (player.shuffleModeEnabled) {
player.unscrambleQueueIndices()
} else {
emptyList()
}
return RawQueue(heap, shuffledMapping, player.currentMediaItemIndex)
}
override val audioSessionId: Int
get() = player.audioSessionId
override val shouldRewindWithPrev: Boolean
get() = playbackSettings.rewindWithPrev && player.currentPosition > REWIND_THRESHOLD
override fun getState(durationMs: Long) =
InternalPlayer.State.from(
player.playWhenReady,
player.isPlaying,
// The position value can be below zero or past the expected duration, make
// sure we handle that.
player.currentPosition.coerceAtLeast(0).coerceAtMost(durationMs))
override fun loadSong(song: Song?, play: Boolean) {
if (song == null) {
// No song, stop playback and foreground state.
logD("Nothing playing, stopping playback")
// For some reason the player does not mark playWhenReady as false when stopped,
// which then completely breaks any re-initialization if playback starts again.
// So we manually set it to false here.
player.playWhenReady = false
player.stop()
stopAndSave()
return
override fun newPlayback(
queue: List<Song>,
start: Song?,
parent: MusicParent?,
shuffled: Boolean
) {
this.parent = parent
player.shuffleModeEnabled = shuffled
player.setMediaItems(queue.map { it.toMediaItem() })
val startIndex =
start
?.let { queue.indexOf(start) }
.also { check(it != -1) { "Start song not in queue" } }
if (shuffled) {
player.setShuffleOrder(BetterShuffleOrder(queue.size, startIndex ?: -1))
}
logD("Loading $song")
player.setMediaItem(MediaItem.fromUri(song.uri))
val target =
startIndex ?: player.currentTimeline.getFirstWindowIndex(player.shuffleModeEnabled)
player.seekTo(target, C.TIME_UNSET)
player.prepare()
player.playWhenReady = play
player.play()
playbackManager.ack(this, StateAck.NewPlayback)
deferSave()
}
override fun playing(playing: Boolean) {
player.playWhenReady = playing
// Dispatched later once all of the changes have been accumulated
// Playing state is not persisted, do not need to save
}
override fun repeatMode(repeatMode: RepeatMode) {
player.repeatMode =
when (repeatMode) {
RepeatMode.NONE -> Player.REPEAT_MODE_OFF
RepeatMode.ALL -> Player.REPEAT_MODE_ALL
RepeatMode.TRACK -> Player.REPEAT_MODE_ONE
}
playbackManager.ack(this, StateAck.RepeatModeChanged)
updatePauseOnRepeat()
deferSave()
}
override fun seekTo(positionMs: Long) {
logD("Seeking to ${positionMs}ms")
player.seekTo(positionMs)
// Dispatched later once all of the changes have been accumulated
// Deferred save is handled on position discontinuity
}
override fun setPlaying(isPlaying: Boolean) {
logD("Updating player state to $isPlaying")
player.playWhenReady = isPlaying
override fun next() {
// Replicate the old pseudo-circular queue behavior when no repeat option is implemented.
// Basically, you can't skip back and wrap around the queue, but you can skip forward and
// wrap around the queue, albeit playback will be paused.
if (player.repeatMode != Player.REPEAT_MODE_OFF || player.hasNextMediaItem()) {
player.seekToNext()
if (!playbackSettings.rememberPause) {
player.play()
}
} else {
goto(0)
// TODO: Dislike the UX implications of this, I feel should I bite the bullet
// and switch to dynamic skip enable/disable?
if (!playbackSettings.rememberPause) {
player.pause()
}
}
playbackManager.ack(this, StateAck.IndexMoved)
// Deferred save is handled on position discontinuity
}
override fun prev() {
if (playbackSettings.rewindWithPrev) {
player.seekToPrevious()
} else {
player.seekToPreviousMediaItem()
}
if (!playbackSettings.rememberPause) {
player.play()
}
playbackManager.ack(this, StateAck.IndexMoved)
// Deferred save is handled on position discontinuity
}
override fun goto(index: Int) {
val indices = player.unscrambleQueueIndices()
if (indices.isEmpty()) {
return
}
val trueIndex = indices[index]
player.seekTo(trueIndex, C.TIME_UNSET)
if (!playbackSettings.rememberPause) {
player.play()
}
playbackManager.ack(this, StateAck.IndexMoved)
// Deferred save is handled on position discontinuity
}
override fun shuffled(shuffled: Boolean) {
logD("Reordering queue to $shuffled")
player.shuffleModeEnabled = shuffled
if (shuffled) {
// Have to manually refresh the shuffle seed and anchor it to the new current songs
player.setShuffleOrder(
BetterShuffleOrder(player.mediaItemCount, player.currentMediaItemIndex))
}
playbackManager.ack(this, StateAck.QueueReordered)
deferSave()
}
override fun playNext(songs: List<Song>, ack: StateAck.PlayNext) {
player.addMediaItems(player.nextMediaItemIndex, songs.map { it.toMediaItem() })
playbackManager.ack(this, ack)
deferSave()
}
override fun addToQueue(songs: List<Song>, ack: StateAck.AddToQueue) {
player.addMediaItems(songs.map { it.toMediaItem() })
playbackManager.ack(this, ack)
deferSave()
}
override fun move(from: Int, to: Int, ack: StateAck.Move) {
val indices = player.unscrambleQueueIndices()
if (indices.isEmpty()) {
return
}
val trueFrom = indices[from]
val trueTo = indices[to]
when {
trueFrom > trueTo -> {
player.moveMediaItem(trueFrom, trueTo)
player.moveMediaItem(trueTo + 1, trueFrom)
}
trueTo > trueFrom -> {
player.moveMediaItem(trueFrom, trueTo)
player.moveMediaItem(trueTo - 1, trueFrom)
}
}
playbackManager.ack(this, ack)
deferSave()
}
override fun remove(at: Int, ack: StateAck.Remove) {
val indices = player.unscrambleQueueIndices()
if (indices.isEmpty()) {
return
}
val trueIndex = indices[at]
val songWillChange = player.currentMediaItemIndex == trueIndex
player.removeMediaItem(trueIndex)
if (songWillChange && !playbackSettings.rememberPause) {
player.play()
}
playbackManager.ack(this, ack)
deferSave()
}
override fun handleDeferred(action: DeferredPlayback): Boolean {
val deviceLibrary =
musicRepository.deviceLibrary
// No library, cannot do anything.
?: return false
when (action) {
// Restore state -> Start a new restoreState job
is DeferredPlayback.RestoreState -> {
logD("Restoring playback state")
restoreScope.launch {
persistenceRepository.readState()?.let {
// Apply the saved state on the main thread to prevent code expecting
// state updates on the main thread from crashing.
withContext(Dispatchers.Main) { playbackManager.applySavedState(it, false) }
}
}
}
// Shuffle all -> Start new playback from all songs
is DeferredPlayback.ShuffleAll -> {
logD("Shuffling all tracks")
playbackManager.play(
null, null, listSettings.songSort.songs(deviceLibrary.songs), true)
}
// Open -> Try to find the Song for the given file and then play it from all songs
is DeferredPlayback.Open -> {
logD("Opening specified file")
deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play(
song,
null,
listSettings.songSort.songs(deviceLibrary.songs),
player.shuffleModeEnabled && playbackSettings.keepShuffle)
}
}
}
return true
}
override fun applySavedState(
parent: MusicParent?,
rawQueue: RawQueue,
ack: StateAck.NewPlayback?
) {
this.parent = parent
player.setMediaItems(rawQueue.heap.map { it.toMediaItem() })
if (rawQueue.isShuffled) {
player.shuffleModeEnabled = true
player.setShuffleOrder(BetterShuffleOrder(rawQueue.shuffledMapping.toIntArray()))
} else {
player.shuffleModeEnabled = false
}
player.seekTo(rawQueue.heapIndex, C.TIME_UNSET)
player.prepare()
ack?.let { playbackManager.ack(this, it) }
}
// --- PLAYER OVERRIDES ---
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted.
hasPlayed = true
logD("Player has started playing")
if (!openAudioEffectSession) {
// Convention to start an audioeffect session on play/pause rather than
// start/stop
logD("Opening audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = true
}
} else if (openAudioEffectSession) {
// Make sure to close the audio session when we stop playback.
logD("Closing audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false
}
}
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
super.onMediaItemTransition(mediaItem, reason)
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO ||
reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) {
playbackManager.ack(this, StateAck.IndexMoved)
}
}
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
if (playbackState == Player.STATE_ENDED && player.repeatMode == Player.REPEAT_MODE_OFF) {
goto(0)
player.pause()
}
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
// TODO: Once position also naturally drifts by some threshold, save
deferSave()
}
}
override fun onEvents(player: Player, events: Player.Events) {
super.onEvents(player, events)
if (events.contains(Player.EVENT_PLAY_WHEN_READY_CHANGED)) {
if (player.playWhenReady) {
// Mark that we have started playing so that the notification can now be posted.
hasPlayed = true
logD("Player has started playing")
if (!openAudioEffectSession) {
// Convention to start an audioeffect session on play/pause rather than
// start/stop
logD("Opening audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = true
}
} else if (openAudioEffectSession) {
// Make sure to close the audio session when we stop playback.
logD("Closing audio effect session")
broadcastAudioEffectAction(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION)
openAudioEffectSession = false
}
}
// Any change to the analogous isPlaying, isAdvancing, or positionMs values require
// us to synchronize with a new state.
if (events.containsAny(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
Player.EVENT_IS_PLAYING_CHANGED,
Player.EVENT_POSITION_DISCONTINUITY)) {
logD("Player state changed, must synchronize state")
playbackManager.synchronizeState(this)
}
}
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED) {
// Player ended, repeat the current track if we are configured to.
if (playbackManager.repeatMode == RepeatMode.TRACK) {
logD("Looping current track")
playbackManager.rewind()
// May be configured to pause when we repeat a track.
if (playbackSettings.pauseOnRepeat) {
logD("Pausing track on loop")
playbackManager.setPlaying(false)
}
} else {
logD("Track ended, moving to next track")
playbackManager.next()
}
playbackManager.ack(this, StateAck.ProgressionChanged)
}
}
@ -316,6 +559,12 @@ class PlaybackService :
playbackManager.next()
}
// --- OTHER OVERRIDES ---
override fun onPauseOnRepeatChanged() {
updatePauseOnRepeat()
}
override fun onMusicChanges(changes: MusicRepository.Changes) {
if (changes.deviceLibrary && musicRepository.deviceLibrary != null) {
// We now have a library, see if we have anything we need to do.
@ -324,71 +573,6 @@ class PlaybackService :
}
}
// --- OTHER FUNCTIONS ---
private fun broadcastAudioEffectAction(event: String) {
logD("Broadcasting AudioEffect event: $event")
sendBroadcast(
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
}
private fun stopAndSave() {
// This session has ended, so we need to reset this flag for when the next session starts.
hasPlayed = false
if (foregroundManager.tryStopForeground()) {
// Now that we have ended the foreground state (and thus music playback), we'll need
// to save the current state as it's not long until this service (and likely the whole
// app) is killed.
logD("Saving playback state")
saveScope.launch { persistenceRepository.saveState(playbackManager.toSavedState()) }
}
}
override fun performAction(action: InternalPlayer.Action): Boolean {
val deviceLibrary =
musicRepository.deviceLibrary
// No library, cannot do anything.
?: return false
when (action) {
// Restore state -> Start a new restoreState job
is InternalPlayer.Action.RestoreState -> {
logD("Restoring playback state")
restoreScope.launch {
persistenceRepository.readState()?.let {
// Apply the saved state on the main thread to prevent code expecting
// state updates on the main thread from crashing.
withContext(Dispatchers.Main) { playbackManager.applySavedState(it, false) }
}
}
}
// Shuffle all -> Start new playback from all songs
is InternalPlayer.Action.ShuffleAll -> {
logD("Shuffling all tracks")
playbackManager.play(
null, null, listSettings.songSort.songs(deviceLibrary.songs), true)
}
// Open -> Try to find the Song for the given file and then play it from all songs
is InternalPlayer.Action.Open -> {
logD("Opening specified file")
deviceLibrary.findSongForUri(application, action.uri)?.let { song ->
playbackManager.play(
song,
null,
listSettings.songSort.songs(deviceLibrary.songs),
playbackManager.queue.isShuffled && playbackSettings.keepShuffle)
}
}
}
return true
}
// --- MEDIASESSIONCOMPONENT OVERRIDES ---
override fun onPostNotification(notification: NotificationComponent) {
// Do not post the notification if playback hasn't started yet. This prevents errors
// where changing a setting would cause the notification to appear in an unfriendly
@ -402,6 +586,104 @@ class PlaybackService :
}
}
// --- PLAYER MANAGEMENT ---
private fun updatePauseOnRepeat() {
player.pauseAtEndOfMediaItems =
playbackManager.repeatMode == RepeatMode.TRACK && playbackSettings.pauseOnRepeat
}
private fun ExoPlayer.unscrambleQueueIndices(): List<Int> {
val timeline = currentTimeline
if (timeline.isEmpty) {
return emptyList()
}
val queue = mutableListOf<Int>()
// Add the active queue item.
val currentMediaItemIndex = currentMediaItemIndex
queue.add(currentMediaItemIndex)
// Fill queue alternating with next and/or previous queue items.
var firstMediaItemIndex = currentMediaItemIndex
var lastMediaItemIndex = currentMediaItemIndex
val shuffleModeEnabled = shuffleModeEnabled
while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET)) {
// Begin with next to have a longer tail than head if an even sized queue needs to be
// trimmed.
if (lastMediaItemIndex != C.INDEX_UNSET) {
lastMediaItemIndex =
timeline.getNextWindowIndex(
lastMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
if (lastMediaItemIndex != C.INDEX_UNSET) {
queue.add(lastMediaItemIndex)
}
}
if (firstMediaItemIndex != C.INDEX_UNSET) {
firstMediaItemIndex =
timeline.getPreviousWindowIndex(
firstMediaItemIndex, Player.REPEAT_MODE_OFF, shuffleModeEnabled)
if (firstMediaItemIndex != C.INDEX_UNSET) {
queue.add(0, firstMediaItemIndex)
}
}
}
return queue
}
private fun Song.toMediaItem() = MediaItem.Builder().setUri(uri).setTag(this).build()
private val MediaItem.song: Song
get() = requireNotNull(localConfiguration).tag as Song
// --- OTHER FUNCTIONS ---
private fun deferSave() {
saveJob {
logD("Waiting for save buffer")
delay(SAVE_BUFFER)
yield()
logD("Committing saved state")
persistenceRepository.saveState(playbackManager.toSavedState())
}
}
private fun saveJob(block: suspend () -> Unit) {
currentSaveJob?.let {
logD("Discarding prior save job")
it.cancel()
}
currentSaveJob = saveScope.launch { block() }
}
private fun broadcastAudioEffectAction(event: String) {
logD("Broadcasting AudioEffect event: $event")
sendBroadcast(
Intent(event)
.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)
.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId)
.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC))
}
private fun endSession() {
// This session has ended, so we need to reset this flag for when the next
// session starts.
saveJob {
logD("Committing saved state")
persistenceRepository.saveState(playbackManager.toSavedState())
withContext(Dispatchers.Main) {
// User could feasibly start playing again if they were fast enough, so
// we need to avoid stopping the foreground state if that's the case.
if (!player.isPlaying) {
hasPlayed = false
playbackManager.playing(false)
foregroundManager.tryStopForeground()
}
}
}
}
/**
* A [BroadcastReceiver] for receiving playback-specific [Intent]s from the system that require
* an active [IntentFilter] to be registered.
@ -437,15 +719,15 @@ class PlaybackService :
// --- AUXIO EVENTS ---
ACTION_PLAY_PAUSE -> {
logD("Received play event")
playbackManager.setPlaying(!playbackManager.playerState.isPlaying)
playbackManager.playing(!playbackManager.progression.isPlaying)
}
ACTION_INC_REPEAT_MODE -> {
logD("Received repeat mode event")
playbackManager.repeatMode = playbackManager.repeatMode.increment()
playbackManager.repeatMode(playbackManager.repeatMode.increment())
}
ACTION_INVERT_SHUFFLE -> {
logD("Received shuffle event")
playbackManager.reorder(!playbackManager.queue.isShuffled)
playbackManager.shuffled(!playbackManager.isShuffled)
}
ACTION_SKIP_PREV -> {
logD("Received skip previous event")
@ -457,8 +739,8 @@ class PlaybackService :
}
ACTION_EXIT -> {
logD("Received exit event")
playbackManager.setPlaying(false)
stopAndSave()
playbackManager.playing(false)
endSession()
}
WidgetProvider.ACTION_WIDGET_UPDATE -> {
logD("Received widget update event")
@ -472,28 +754,28 @@ class PlaybackService :
// which would result in unexpected playback. Work around it by dropping the first
// call to this function, which should come from that Intent.
if (playbackSettings.headsetAutoplay &&
playbackManager.queue.currentSong != null &&
playbackManager.currentSong != null &&
initialHeadsetPlugEventHandled) {
logD("Device connected, resuming")
playbackManager.setPlaying(true)
playbackManager.playing(true)
}
}
private fun pauseFromHeadsetPlug() {
if (playbackManager.queue.currentSong != null) {
if (playbackManager.currentSong != null) {
logD("Device disconnected, pausing")
playbackManager.setPlaying(false)
playbackManager.playing(false)
}
}
}
companion object {
const val SAVE_BUFFER = 5000L
const val ACTION_INC_REPEAT_MODE = BuildConfig.APPLICATION_ID + ".action.LOOP"
const val ACTION_INVERT_SHUFFLE = BuildConfig.APPLICATION_ID + ".action.SHUFFLE"
const val ACTION_SKIP_PREV = BuildConfig.APPLICATION_ID + ".action.PREV"
const val ACTION_PLAY_PAUSE = BuildConfig.APPLICATION_ID + ".action.PLAY_PAUSE"
const val ACTION_SKIP_NEXT = BuildConfig.APPLICATION_ID + ".action.NEXT"
const val ACTION_EXIT = BuildConfig.APPLICATION_ID + ".action.EXIT"
private const val REWIND_THRESHOLD = 3000L
}
}

View file

@ -68,6 +68,9 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
binding.aboutLicenses.setOnClickListener { requireContext().openInBrowser(LINK_LICENSES) }
binding.aboutProfile.setOnClickListener { requireContext().openInBrowser(LINK_PROFILE) }
binding.aboutDonate.setOnClickListener { requireContext().openInBrowser(LINK_DONATE) }
binding.aboutSupporterYrliet.setOnClickListener {
requireContext().openInBrowser(LINK_YRLIET)
}
binding.aboutSupportersPromo.setOnClickListener {
requireContext().openInBrowser(LINK_DONATE)
}
@ -97,5 +100,6 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
const val LINK_LICENSES = "$LINK_WIKI/Licenses"
const val LINK_PROFILE = "https://github.com/OxygenCobalt"
const val LINK_DONATE = "https://github.com/sponsors/OxygenCobalt"
const val LINK_YRLIET = "https://github.com/yrliet"
}
}

View file

@ -32,7 +32,6 @@ import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.ui.WrappedDialogPreference
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.navigateSafe
import org.oxycblt.auxio.util.showToast
/**
* The [PreferenceFragmentCompat] that displays the root settings list.
@ -84,41 +83,6 @@ class RootPreferenceFragment : BasePreferenceFragment(R.xml.preferences_root) {
}
getString(R.string.set_key_reindex) -> musicModel.refresh()
getString(R.string.set_key_rescan) -> musicModel.rescan()
getString(R.string.set_key_save_state) -> {
playbackModel.savePlaybackState { saved ->
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
logD("Showing saving confirmation")
if (saved) {
context?.showToast(R.string.lbl_state_saved)
} else {
context?.showToast(R.string.err_did_not_save)
}
}
}
getString(R.string.set_key_wipe_state) -> {
playbackModel.wipePlaybackState { wiped ->
logD("Showing wipe confirmation")
if (wiped) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
context?.showToast(R.string.lbl_state_wiped)
} else {
context?.showToast(R.string.err_did_not_wipe)
}
}
}
getString(R.string.set_key_restore_state) ->
playbackModel.tryRestorePlaybackState { restored ->
logD("Showing restore confirmation")
if (restored) {
// Use the nullable context, as we could try to show a toast when this
// fragment is no longer attached.
context?.showToast(R.string.lbl_state_restored)
} else {
context?.showToast(R.string.err_did_not_restore)
}
}
else -> return super.onPreferenceTreeClick(preference)
}

View file

@ -152,6 +152,8 @@ private fun Fragment.launch(
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
}
const val DEFAULT_TIMEOUT = 60000L
/**
* Wraps [SendChannel.send] with a specified timeout.
*
@ -160,7 +162,7 @@ private fun Fragment.launch(
* @throws TimeoutException If the timeout is reached, provides context on what element
* specifically.
*/
suspend fun <E> SendChannel<E>.sendWithTimeout(element: E, timeout: Long = 10000) {
suspend fun <E> SendChannel<E>.sendWithTimeout(element: E, timeout: Long = DEFAULT_TIMEOUT) {
try {
withTimeout(timeout) { send(element) }
} catch (e: TimeoutCancellationException) {
@ -179,7 +181,7 @@ suspend fun <E> SendChannel<E>.sendWithTimeout(element: E, timeout: Long = 10000
* specifically.
*/
suspend fun <E> ReceiveChannel<E>.forEachWithTimeout(
timeout: Long = 10000,
timeout: Long = DEFAULT_TIMEOUT,
action: suspend (E) -> Unit
) {
var exhausted = false

View file

@ -31,9 +31,9 @@ import org.oxycblt.auxio.image.extractor.RoundedRectTransformation
import org.oxycblt.auxio.image.extractor.SquareCropTransformation
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.queue.Queue
import org.oxycblt.auxio.playback.state.InternalPlayer
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.Progression
import org.oxycblt.auxio.playback.state.QueueChange
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.getDimenPixels
@ -64,7 +64,7 @@ constructor(
/** Update [WidgetProvider] with the current playback state. */
fun update() {
val song = playbackManager.queue.currentSong
val song = playbackManager.currentSong
if (song == null) {
logD("No song, resetting widget")
widgetProvider.update(context, uiSettings, null)
@ -72,9 +72,9 @@ constructor(
}
// Note: Store these values here so they remain consistent once the bitmap is loaded.
val isPlaying = playbackManager.playerState.isPlaying
val isPlaying = playbackManager.progression.isPlaying
val repeatMode = playbackManager.repeatMode
val isShuffled = playbackManager.queue.isShuffled
val isShuffled = playbackManager.isShuffled
logD("Updating widget with new playback state")
bitmapProvider.load(
@ -136,15 +136,26 @@ constructor(
// --- CALLBACKS ---
// Respond to all major song or player changes that will affect the widget
override fun onIndexMoved(queue: Queue) = update()
override fun onIndexMoved(index: Int) = update()
override fun onQueueReordered(queue: Queue) = update()
override fun onQueueChanged(queue: List<Song>, index: Int, change: QueueChange) {
if (change.type == QueueChange.Type.SONG) {
update()
}
}
override fun onNewPlayback(queue: Queue, parent: MusicParent?) = update()
override fun onQueueReordered(queue: List<Song>, index: Int, isShuffled: Boolean) = update()
override fun onStateChanged(state: InternalPlayer.State) = update()
override fun onNewPlayback(
parent: MusicParent?,
queue: List<Song>,
index: Int,
isShuffled: Boolean
) = update()
override fun onRepeatChanged(repeatMode: RepeatMode) = update()
override fun onProgressionChanged(progression: Progression) = update()
override fun onRepeatModeChanged(repeatMode: RepeatMode) = update()
// Respond to settings changes that will affect the widget
override fun onRoundModeChanged() = update()
@ -154,11 +165,12 @@ constructor(
/**
* A condensed form of the playback state that is safe to use in AppWidgets.
*
* @param song [Queue.currentSong]
* @param song [PlaybackStateManager.currentSong]
* @param cover A pre-loaded album cover [Bitmap] for [song].
* @param isPlaying [PlaybackStateManager.playerState]
* @param cover A pre-loaded album cover [Bitmap] for [song], with rounded corners.
* @param isPlaying [PlaybackStateManager.progression]
* @param repeatMode [PlaybackStateManager.repeatMode]
* @param isShuffled [Queue.isShuffled]
* @param isShuffled [PlaybackStateManager.isShuffled]
*/
data class PlaybackState(
val song: Song,

View file

@ -91,11 +91,14 @@ class WidgetProvider : AppWidgetProvider() {
// the widget elements, plus some leeway for text sizing.
val views =
mapOf(
SizeF(180f, 100f) to newThinLayout(context, uiSettings, state),
SizeF(180f, 152f) to newSmallLayout(context, uiSettings, state),
SizeF(272f, 152f) to newWideLayout(context, uiSettings, state),
SizeF(180f, 272f) to newMediumLayout(context, uiSettings, state),
SizeF(272f, 272f) to newLargeLayout(context, uiSettings, state))
SizeF(180f, 48f) to newThinStickLayout(context, state),
SizeF(304f, 48f) to newWideStickLayout(context, state),
SizeF(180f, 100f) to newThinWaferLayout(context, uiSettings, state),
SizeF(304f, 100f) to newWideWaferLayout(context, uiSettings, state),
SizeF(180f, 152f) to newThinDockedLayout(context, uiSettings, state),
SizeF(304f, 152f) to newWideDockedLayout(context, uiSettings, state),
SizeF(180f, 272f) to newThinPaneLayout(context, uiSettings, state),
SizeF(304f, 272f) to newWidePaneLayout(context, uiSettings, state))
// Manually update AppWidgetManager with the new views.
val awm = AppWidgetManager.getInstance(context)
@ -139,60 +142,78 @@ class WidgetProvider : AppWidgetProvider() {
private fun newDefaultLayout(context: Context) =
newRemoteViews(context, R.layout.widget_default)
private fun newThinLayout(
private fun newThinStickLayout(context: Context, state: WidgetComponent.PlaybackState) =
newRemoteViews(context, R.layout.widget_stick_thin).setupTimelineControls(context, state)
private fun newWideStickLayout(context: Context, state: WidgetComponent.PlaybackState) =
newRemoteViews(context, R.layout.widget_stick_wide).setupFullControls(context, state)
private fun newThinWaferLayout(
context: Context,
uiSettings: UISettings,
state: WidgetComponent.PlaybackState
) =
newRemoteViews(context, R.layout.widget_thin)
newRemoteViews(context, R.layout.widget_wafer_thin)
.setupBackground(
uiSettings,
)
.setupPlaybackState(context, state)
.setupCover(context, state.takeIf { canDisplayWaferCover(uiSettings) })
.setupTimelineControls(context, state)
private fun newSmallLayout(
private fun newWideWaferLayout(
context: Context,
uiSettings: UISettings,
state: WidgetComponent.PlaybackState
) =
newRemoteViews(context, R.layout.widget_small)
newRemoteViews(context, R.layout.widget_wafer_wide)
.setupBackground(
uiSettings,
)
.setupCover(context, state.takeIf { canDisplayWaferCover(uiSettings) })
.setupFullControls(context, state)
private fun newThinDockedLayout(
context: Context,
uiSettings: UISettings,
state: WidgetComponent.PlaybackState
) =
newRemoteViews(context, R.layout.widget_docked_thin)
.setupBar(
uiSettings,
)
.setupCover(context, state)
.setupTimelineControls(context, state)
private fun newMediumLayout(
private fun newWideDockedLayout(
context: Context,
uiSettings: UISettings,
state: WidgetComponent.PlaybackState
) =
newRemoteViews(context, R.layout.widget_medium)
.setupBackground(
uiSettings,
)
.setupPlaybackState(context, state)
.setupTimelineControls(context, state)
private fun newWideLayout(
context: Context,
uiSettings: UISettings,
state: WidgetComponent.PlaybackState
) =
newRemoteViews(context, R.layout.widget_wide)
newRemoteViews(context, R.layout.widget_docked_wide)
.setupBar(
uiSettings,
)
.setupCover(context, state)
.setupFullControls(context, state)
private fun newLargeLayout(
private fun newThinPaneLayout(
context: Context,
uiSettings: UISettings,
state: WidgetComponent.PlaybackState
) =
newRemoteViews(context, R.layout.widget_large)
newRemoteViews(context, R.layout.widget_pane_thin)
.setupBackground(
uiSettings,
)
.setupPlaybackState(context, state)
.setupTimelineControls(context, state)
private fun newWidePaneLayout(
context: Context,
uiSettings: UISettings,
state: WidgetComponent.PlaybackState
) =
newRemoteViews(context, R.layout.widget_pane_wide)
.setupBackground(
uiSettings,
)
@ -246,8 +267,14 @@ class WidgetProvider : AppWidgetProvider() {
*/
private fun RemoteViews.setupCover(
context: Context,
state: WidgetComponent.PlaybackState
state: WidgetComponent.PlaybackState?
): RemoteViews {
if (state == null) {
setImageViewBitmap(R.id.widget_cover, null)
setContentDescription(R.id.widget_cover, null)
return this
}
if (state.cover != null) {
setImageViewBitmap(R.id.widget_cover, state.cover)
setContentDescription(
@ -388,6 +415,18 @@ class WidgetProvider : AppWidgetProvider() {
return this
}
private fun useRoundedRemoteViews(uiSettings: UISettings) =
uiSettings.roundMode || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
private fun canDisplayWaferCover(uiSettings: UISettings) =
// We cannot display album covers in the wafer-style widget when round mode is enabled
// below Android 12, as:
// - We cannot rely on system widget corner clipping, like on Android 12+
// - We cannot manually clip the widget ourselves due to broken clipToOutline support
// - We cannot determine the exact widget height that would allow us to clip the loaded
// image itself
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || !uiSettings.roundMode
companion object {
/**
* Broadcast when [WidgetProvider] desires to update it's widget with new information.

View file

@ -28,7 +28,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import kotlin.math.sqrt
import org.oxycblt.auxio.ui.UISettings
import org.oxycblt.auxio.util.isLandscape
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent
@ -139,13 +138,3 @@ fun AppWidgetManager.updateAppWidgetCompat(
}
}
}
/**
* Returns whether rounded UI elements are appropriate for the widget, either based on the current
* settings or if the widget has to fit in aesthetically with other widgets.
*
* @param [uiSettings] [UISettings] required to obtain round mode configuration.
* @return true if to use round mode, false otherwise.
*/
fun useRoundedRemoteViews(uiSettings: UISettings) =
uiSettings.roundMode || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurface" />
<corners android:radius="@android:dimen/system_app_widget_background_radius" />
</shape>

View file

@ -4,10 +4,17 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorSurfaceInverse"
android:pathData="M12,16.5Q13.875,16.5 15.188,15.188Q16.5,13.875 16.5,12Q16.5,10.125 15.188,8.812Q13.875,7.5 12,7.5Q10.125,7.5 8.812,8.812Q7.5,10.125 7.5,12Q7.5,13.875 8.812,15.188Q10.125,16.5 12,16.5ZM12,13Q11.575,13 11.288,12.712Q11,12.425 11,12Q11,11.575 11.288,11.287Q11.575,11 12,11Q12.425,11 12.713,11.287Q13,11.575 13,12Q13,12.425 12.713,12.712Q12.425,13 12,13ZM12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22ZM12,20Q15.35,20 17.675,17.675Q20,15.35 20,12Q20,8.65 17.675,6.325Q15.35,4 12,4Q8.65,4 6.325,6.325Q4,8.65 4,12Q4,15.35 6.325,17.675Q8.65,20 12,20ZM12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Q12,12 12,12Z" />
<path
android:fillColor="?attr/colorOnSurfaceInverse"
android:pathData="M 11.999784 1.9998779 A 10 9.999999 0 0 1 22.000208 11.999784 C 22.000208 10.616452 21.737475 9.3164294 21.212142 8.099764 C 20.687476 6.8830985 19.974804 5.8247631 19.074805 4.924764 C 18.174806 4.0247649 17.11647 3.3122428 15.899805 2.78691 C 14.683139 2.2622439 13.383116 1.9998779 11.999784 1.9998779 z M 11.999784 1.9998779 C 10.616452 1.9998779 9.3164294 2.2622439 8.099764 2.78691 C 6.8830985 3.3122428 5.8247631 4.0247649 4.924764 4.924764 C 4.0247649 5.8247631 3.3126097 6.8830985 2.7879435 8.099764 C 2.2626107 9.3164294 1.9998779 10.616452 1.9998779 11.999784 A 10 9.999999 0 0 1 11.999784 1.9998779 z M 1.9998779 11.999784 C 1.9998779 13.383116 2.2626107 14.683139 2.7879435 15.899805 C 3.3126097 17.11647 4.0247649 18.174806 4.924764 19.074805 C 5.8247631 19.974804 6.8830985 20.687476 8.099764 21.212142 C 9.3164294 21.737475 10.616452 22.000208 11.999784 22.000208 A 10 9.999999 0 0 1 1.9998779 11.999784 z M 11.999784 22.000208 C 13.383116 22.000208 14.683139 21.737475 15.899805 21.212142 C 17.11647 20.687476 18.174806 19.974804 19.074805 19.074805 C 19.974804 18.174806 20.687476 17.11647 21.212142 15.899805 C 21.737475 14.683139 22.000208 13.383116 22.000208 11.999784 A 10 9.999999 0 0 1 11.999784 22.000208 z M 11.999784 3.9997559 C 9.7664532 3.9997559 7.8751938 4.7751969 6.3251953 6.3251953 C 4.7751969 7.8751938 3.9997559 9.7664532 3.9997559 11.999784 C 3.9997559 14.233115 4.7751969 16.124892 6.3251953 17.67489 C 7.8751938 19.224889 9.7664532 19.999813 11.999784 19.999813 C 14.233115 19.999813 16.124892 19.224889 17.67489 17.67489 C 19.224889 16.124892 19.999813 14.233115 19.999813 11.999784 C 19.999813 9.7664532 19.224889 7.8751938 17.67489 6.3251953 C 16.124892 4.7751969 14.233115 3.9997559 11.999784 3.9997559 z M 11.999784 7.4998006 C 13.249783 7.4998006 14.312888 7.9371994 15.18822 8.8118652 C 16.062886 9.6871977 16.499768 10.749786 16.499768 11.999784 C 16.499768 13.249783 16.062886 14.312888 15.18822 15.18822 C 14.312888 16.062886 13.249783 16.499768 11.999784 16.499768 C 10.749786 16.499768 9.6871977 16.062886 8.8118652 15.18822 C 7.9371994 14.312888 7.4998006 13.249783 7.4998006 11.999784 C 7.4998006 10.749786 7.9371994 9.6871977 8.8118652 8.8118652 C 9.6871977 7.9371994 10.749786 7.4998006 11.999784 7.4998006 z M 11.999784 10.999845 C 11.716451 10.999845 11.479533 11.095833 11.2882 11.287166 C 11.0962 11.479166 10.999845 11.716451 10.999845 11.999784 C 10.999845 12.283117 11.0962 12.520552 11.2882 12.711886 C 11.479533 12.903885 11.716451 13.00024 11.999784 13.00024 C 12.283117 13.00024 12.520919 12.903885 12.712919 12.711886 C 12.904252 12.520552 13.00024 12.283117 13.00024 11.999784 C 13.00024 11.716451 12.904252 11.479166 12.712919 11.287166 C 12.520919 11.095833 12.283117 10.999845 11.999784 10.999845 z " />
android:pathData="M 2.6000008,9.3143836e-7 H 21.399999 c 1.4404,0 2.6,1.15959996856164 2.6,2.59999986856164 V 21.399999 c 0,1.4404 -1.1596,2.6 -2.6,2.6 H 2.6000008 c -1.4403999,0 -2.59999986856164,-1.1596 -2.59999986856164,-2.6 V 2.6000008 C 9.3143836e-7,1.1596009 1.1596009,9.3143836e-7 2.6000008,9.3143836e-7 Z" />
<group
android:scaleX="0.5"
android:scaleY="0.5"
android:translateX="6"
android:translateY="6">
<path
android:fillColor="?attr/colorSurfaceInverse"
android:pathData="M10,21Q8.35,21 7.175,19.825Q6,18.65 6,17Q6,15.35 7.175,14.175Q8.35,13 10,13Q10.575,13 11.062,13.137Q11.55,13.275 12,13.55V3H18V7H14V17Q14,18.65 12.825,19.825Q11.65,21 10,21Z" />
</group>
</vector>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?attr/colorSurface" />
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSurface" />
<corners android:radius="@dimen/spacing_medium" />
</shape>

View file

@ -182,7 +182,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lbl_author_name"
app:drawableStartCompat="@drawable/ic_author_24"
app:drawableStartCompat="@drawable/ic_person_24"
app:drawableTint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -224,6 +224,18 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/about_supporter_yrliet"
style="@style/Widget.Auxio.TextView.Icon.Clickable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/sup_yrliet"
app:drawableStartCompat="@drawable/ic_person_24"
app:drawableTint="?attr/colorControlNormal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/about_licenses" />
<TextView
android:id="@+id/about_supporters_promo"
style="@style/Widget.Auxio.TextView.Icon.Clickable"

View file

@ -164,14 +164,16 @@
android:clickable="true"
android:focusable="true"
android:gravity="bottom|end"
android:contentDescription="@string/lbl_new_playlist"
app:sdMainFabAnimationRotateAngle="135"
app:sdMainFabClosedIconColor="@android:color/white"
app:sdMainFabClosedSrc="@drawable/ic_add_24"/>
app:sdMainFabClosedSrc="@drawable/ic_add_24" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/home_shuffle_fab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/lbl_shuffle"
android:layout_gravity="bottom|end"
android:layout_margin="@dimen/spacing_medium"
android:src="@drawable/ic_shuffle_off_24" />

View file

@ -18,7 +18,10 @@
app:navGraph="@navigation/inner"
tools:layout="@layout/fragment_home" />
<View android:id="@+id/main_scrim" android:layout_height="match_parent" android:layout_width="match_parent"/>
<View
android:id="@+id/main_scrim"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/playback_sheet"
@ -83,7 +86,10 @@
</LinearLayout>
<View android:id="@+id/sheet_scrim" android:layout_height="match_parent" android:layout_width="match_parent"/>
<View
android:id="@+id/sheet_scrim"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal"
android:theme="@style/Theme.Auxio.Widget">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/ui_widget_rectangle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev_24" />
</FrameLayout>
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.MaterialButton.AppWidget.PlayPause"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play_24" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/ui_widget_rectangle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next_24" />
</FrameLayout>
</LinearLayout>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal"
android:theme="@style/Theme.Auxio.Widget">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/ui_widget_rectangle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_repeat"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/desc_change_repeat"
android:src="@drawable/ic_repeat_off_24" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/ui_widget_rectangle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev_24" />
</FrameLayout>
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.MaterialButton.AppWidget.PlayPause"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play_24" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/ui_widget_rectangle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next_24" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@drawable/ui_widget_rectangle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_shuffle"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/desc_shuffle"
android:src="@drawable/ic_shuffle_off_24" />
</FrameLayout>
</LinearLayout>

View file

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ui_widget_bg_system"
android:backgroundTint="?attr/colorSurface"
android:baselineAligned="false"
android:orientation="horizontal"
android:theme="@style/Theme.Auxio.Widget">
<!--
Wrapping the 1:1 ImageView hack in a LinearLayout allows the view to measure greedily
without squishing the controls.
-->
<android.widget.RelativeLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<!--
See widget_small.xml for an explanation for the ImageView setup.
-->
<android.widget.ImageView
android:id="@+id/widget_aspect_ratio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="@dimen/spacing_medium"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_medium"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:src="@drawable/ui_remote_aspect_ratio"
android:visibility="invisible"
tools:ignore="ContentDescription" />
<android.widget.ImageView
android:id="@+id/widget_cover"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignStart="@id/widget_aspect_ratio"
android:layout_alignTop="@id/widget_aspect_ratio"
android:layout_alignEnd="@id/widget_aspect_ratio"
android:layout_alignBottom="@id/widget_aspect_ratio"
android:src="@drawable/ic_remote_default_cover_24"
tools:ignore="ContentDescription" />
</android.widget.RelativeLayout>
<android.widget.LinearLayout
android:id="@+id/widget_panel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/spacing_medium"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_marginBottom="@dimen/spacing_medium"
android:layout_weight="2"
android:orientation="horizontal">
<android.widget.LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="@dimen/spacing_medium"
android:layout_weight="1"
android:orientation="vertical">
<android.widget.TextView
android:id="@+id/widget_song"
style="@style/Widget.Auxio.TextView.Primary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Song name" />
<android.widget.TextView
android:id="@+id/widget_artist"
style="@style/Widget.Auxio.TextView.Secondary.AppWidget"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Artist name" />
</android.widget.LinearLayout>
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.MaterialButton.AppWidget.PlayPause"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play_24" />
</android.widget.LinearLayout>
</LinearLayout>

View file

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ui_widget_bg_system"
android:backgroundTint="?attr/colorSurface"
android:baselineAligned="false"
android:orientation="horizontal"
android:theme="@style/Theme.Auxio.Widget">
<!--
Wrapping the 1:1 ImageView hack in a LinearLayout allows the view to measure greedily
without squishing the controls.
-->
<android.widget.ImageView
android:id="@+id/widget_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:ignore="ContentDescription" />
<android.widget.LinearLayout
android:id="@+id/widget_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="@dimen/spacing_mid_medium"
android:orientation="horizontal">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ui_widget_circle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev_24" />
</FrameLayout>
<android.widget.ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.MaterialButton.AppWidget.PlayPause"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play_24" />
<android.widget.ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ui_widget_circle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next_24" />
</FrameLayout>
</android.widget.LinearLayout>
</FrameLayout>

View file

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/ui_widget_bg_system"
android:backgroundTint="?attr/colorSurface"
android:baselineAligned="false"
android:orientation="horizontal"
android:theme="@style/Theme.Auxio.Widget">
<!--
Wrapping the 1:1 ImageView hack in a LinearLayout allows the view to measure greedily
without squishing the controls.
-->
<android.widget.ImageView
android:id="@+id/widget_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:ignore="ContentDescription" />
<android.widget.LinearLayout
android:id="@+id/widget_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:padding="@dimen/spacing_mid_medium"
android:orientation="horizontal">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ui_widget_circle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_repeat"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_change_repeat"
android:src="@drawable/ic_repeat_off_24" />
</FrameLayout>
<android.widget.ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ui_widget_circle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_prev"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_prev"
android:src="@drawable/ic_skip_prev_24" />
</FrameLayout>
<android.widget.ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<android.widget.ImageButton
android:id="@+id/widget_play_pause"
style="@style/Widget.Auxio.MaterialButton.AppWidget.PlayPause"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_play_pause"
android:src="@drawable/ic_play_24" />
<android.widget.ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ui_widget_circle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_skip_next"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_skip_next"
android:src="@drawable/ic_skip_next_24" />
</FrameLayout>
<android.widget.ImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ui_widget_circle_button_bg">
<android.widget.ImageButton
android:id="@+id/widget_shuffle"
style="@style/Widget.Auxio.MaterialButton.AppWidget"
android:layout_width="@dimen/size_btn"
android:layout_height="@dimen/size_btn"
android:contentDescription="@string/desc_shuffle"
android:src="@drawable/ic_shuffle_off_24" />
</FrameLayout>
</android.widget.LinearLayout>
</FrameLayout>

View file

@ -319,4 +319,13 @@
<string name="lng_playlist_exported">Плэйліст экспартаваны</string>
<string name="lbl_export_playlist">Экспартаваць плэйліст</string>
<string name="err_export_failed">Немагчыма экспартаваць плэйліст ў гэты файл</string>
<string name="lbl_import_playlist">Імпартаваць плэйліст</string>
<string name="lbl_replaygain_track">Рэгуляванне ReplayGain песні</string>
<string name="lbl_replaygain_album">Рэгуляванне ReplayGain альбома</string>
<string name="lbl_author">Аўтар</string>
<string name="lbl_donate">Ахвярнасць</string>
<string name="lbl_supporters">Прыхільнікі</string>
<string name="lng_supporters_promo">Ахвяруйце на праект, каб ваша імя было дададзена тут!</string>
<string name="set_remember_pause">Запамінаць паўзу</string>
<string name="set_remember_pause_desc">Пакідаць прайграванне/паўзу падчас пропуску або рэдагаванні чаргі</string>
</resources>

View file

@ -330,4 +330,13 @@
<string name="lbl_import">Import</string>
<string name="lng_playlist_imported">Seznam skladeb importován</string>
<string name="lng_playlist_exported">Seznam skladeb exportován</string>
<string name="lbl_import_playlist">Importovat seznam skladeb</string>
<string name="lbl_replaygain_track">Úprava ReplayGain u stopy</string>
<string name="lbl_donate">Přispět</string>
<string name="lbl_supporters">Podporovatelé</string>
<string name="lbl_author">Autor</string>
<string name="lbl_replaygain_album">Úprava ReplayGain u alba</string>
<string name="lng_supporters_promo">Přispějte na projekt a uvidíte zde své jméno!</string>
<string name="set_remember_pause_desc">Zůstat ve stavu přehrávání/pozastavení při přeskakování nebo úpravě fronty</string>
<string name="set_remember_pause">Zapamatovat pozastavení</string>
</resources>

View file

@ -311,4 +311,23 @@
<string name="lbl_path">Pfad</string>
<string name="err_import_failed">Wiedergabeliste konnte nicht aus dieser Datei importiert werden</string>
<string name="lbl_empty_playlist">Leere Wiedergabeliste</string>
<string name="lbl_replaygain_album">ReplayGain-Albenanpassung</string>
<string name="lbl_replaygain_track">ReplayGain-Trackanpassung</string>
<string name="lbl_author">Autor</string>
<string name="lbl_donate">Spenden</string>
<string name="lbl_supporters">Unterstützer</string>
<string name="lng_supporters_promo">Spende für das Projekt, damit dein Name hier aufgenommen wird!</string>
<string name="lbl_import_playlist">Wiedergabeliste importieren</string>
<string name="lbl_export_playlist">Wiedergabeliste exportieren</string>
<string name="lbl_path_style">Pfadstil</string>
<string name="lbl_path_style_absolute">Abolut</string>
<string name="lbl_path_style_relative">Relativ</string>
<string name="lbl_windows_paths">Windows-kompatible Pfade verwenden</string>
<string name="lbl_export">Exportieren</string>
<string name="err_export_failed">Wiedergabeliste konnte nicht in diese Datei exportiert werden</string>
<string name="lbl_import">Importieren</string>
<string name="lng_playlist_imported">Wiedergabeliste importiert</string>
<string name="lng_playlist_exported">Wiedergabeliste exportiert</string>
<string name="set_remember_pause">Pause merken</string>
<string name="set_remember_pause_desc">Wiedergabe/Pause beim Springen oder Bearbeiten der Warteschlange beibehalten</string>
</resources>

View file

@ -325,4 +325,13 @@
<string name="lng_playlist_exported">Lista de reproducción exportada</string>
<string name="lbl_export_playlist">Exportar lista de reproducción</string>
<string name="err_export_failed">No se puede exportar la lista de reproducción a este archivo</string>
<string name="lbl_import_playlist">Importar lista de reproducción</string>
<string name="lbl_replaygain_track">Ajuste de pista de ganancia de reproducción</string>
<string name="lbl_donate">Donar</string>
<string name="lbl_supporters">Partidarios</string>
<string name="lbl_replaygain_album">Ajuste del álbum de ganancia de reproducción</string>
<string name="lbl_author">Autor</string>
<string name="lng_supporters_promo">¡Haga una donación al proyecto para que agreguen su nombre aquí!</string>
<string name="set_remember_pause">Recordar la pausa</string>
<string name="set_remember_pause_desc">Permanecer en reproducción/pausa al saltar o editar la cola</string>
</resources>

View file

@ -272,4 +272,31 @@
<string name="lbl_show_error_info">Lisää</string>
<string name="lbl_copied">Kopioitu</string>
<string name="lbl_report">Ilmoita virheestä</string>
<string name="lbl_selection">Valinta</string>
<string name="lbl_imported_playlist">Tuotu soittolista</string>
<string name="lbl_author">Tekijä</string>
<string name="lbl_donate">Lahjoita</string>
<string name="lbl_supporters">Tukijat</string>
<string name="lng_playlist_imported">Soittolista tuotu</string>
<string name="lng_playlist_exported">Soittolista viety</string>
<string name="lng_supporters_promo">Lahjoita projektille saadaksesi nimesi näkyviin tähän!</string>
<string name="def_album_count">Ei albumeja</string>
<string name="lbl_empty_playlist">Tyhjä soittolista</string>
<string name="lbl_import_playlist">Tuo soittolista</string>
<string name="lbl_path">Polku</string>
<string name="lbl_import">Tuo</string>
<string name="lbl_export">Vie</string>
<string name="lbl_export_playlist">Vie soittolista</string>
<string name="lbl_sort_mode">Järjestys</string>
<string name="lbl_sort_direction">Suunta</string>
<string name="lbl_path_style">Polun tyyli</string>
<string name="lbl_path_style_absolute">Absoluuttinen</string>
<string name="lbl_path_style_relative">Suhteellinen</string>
<string name="lbl_windows_paths">Käytä Windows-yhteensopivia polkuja</string>
<string name="set_notif_action">Mukautettu ilmoituksen toiminto</string>
<string name="lbl_replaygain_track">ReplayGain-kappalesäätö</string>
<string name="set_remember_pause">Muista keskeytys</string>
<string name="lbl_replaygain_album">ReplayGain-albumisäätö</string>
<string name="err_import_failed">Soittolistan tuonti tästä tiedostosta ei onnistu</string>
<string name="err_export_failed">Soittolistan vienti tähän tiedostoon ei onnistu</string>
</resources>

View file

@ -44,7 +44,7 @@
</plurals>
<plurals name="fmt_album_count">
<item quantity="one">%d एल्बम</item>
<item quantity="other">%d एल्बम</item>
<item quantity="other">%d एल्बम</item>
</plurals>
<string name="lbl_name">नाम</string>
<string name="lbl_genre">शैली</string>
@ -58,7 +58,7 @@
<string name="lbl_date_added">तिथि जोड़ी गई</string>
<string name="lbl_indexer">गाने लोड हो रहे है</string>
<string name="lbl_indexing">गाने लोड हो रहे है</string>
<string name="info_app_desc">एंड्रॉयड के लिए एक सीधा साधा, विवेकशील गाने बजाने वाला ऐप।</string>
<string name="info_app_desc">एंड्रॉयड के लिए एक सीधा साधा,विवेकशील गाने बजाने वाला ऐप।</string>
<string name="lbl_new_playlist">नई प्लेलिस्ट</string>
<string name="lbl_play_next">अगला चलाएं</string>
<string name="set_lib_tabs">लायब्रेरी टैब्स</string>
@ -320,4 +320,13 @@
<string name="lng_playlist_exported">प्लेलिस्ट एक्सपोर्ट की गई</string>
<string name="lbl_export_playlist">प्लेलिस्ट एक्सपोर्ट करें</string>
<string name="err_export_failed">प्लेलिस्ट को इस फ़ाइल में एक्सपोर्ट करने में असमर्थ</string>
<string name="lbl_import_playlist">प्लेलिस्ट इम्पोर्ट करें</string>
<string name="lbl_replaygain_track">रीप्लेगेन ट्रैक एडजस्टमेंट</string>
<string name="lbl_replaygain_album">रीप्लेगेन एल्बम एडजस्टमेंट</string>
<string name="lbl_supporters">समर्थक</string>
<string name="lbl_author">लेखक</string>
<string name="lbl_donate">दान करें</string>
<string name="lng_supporters_promo">अपना नाम यहां जुड़वाने के लिए परियोजना में दान करें!</string>
<string name="set_remember_pause">विराम याद रखें</string>
<string name="set_remember_pause_desc">कतार छोड़ते या संपादित करते समय चलता/रोका रखिए</string>
</resources>

View file

@ -71,11 +71,11 @@
<string name="set_audio">Zvuk</string>
<string name="set_headset_autoplay">Slušalice: odmah reproduciraj</string>
<string name="set_headset_autoplay_desc">Uvijek pokreni reprodukciju kada su slušalice povezane (možda neće raditi na svim uređajima)</string>
<string name="set_replay_gain_mode">Strategija pojačanja</string>
<string name="set_replay_gain_mode">ReplayGain strategija</string>
<string name="set_replay_gain_mode_track">Preferiraj zvučni zapis</string>
<string name="set_replay_gain_mode_album">Preferiraj album</string>
<string name="set_replay_gain_mode_dynamic">Ako se reproducira album, preferiraj album</string>
<string name="set_pre_amp">Pretpojačalo pojačanja</string>
<string name="set_pre_amp">ReplayGain pretpojačalo</string>
<string name="set_pre_amp_desc">Pretpojačalo je tijekom reprodukcije primijenjeno postojećoj prilagodbi</string>
<string name="set_pre_amp_with">Prilagođavanje s oznakama</string>
<string name="set_pre_amp_without">Prilagođavanje bez oznaka</string>
@ -193,10 +193,10 @@
<string name="set_keep_shuffle">Zapamti miješanje glazbe</string>
<string name="set_restore_desc">Vrati prethodno spremljeno stanje reprodukcije (ako postoji)</string>
<string name="set_play_song_from_album">Reproduciraj iz albuma</string>
<string name="set_repeat_pause_desc">Pauziraj čim se pjesma ponovi</string>
<string name="set_repeat_pause_desc">Pauziraj pri ponavljanju pjesme</string>
<string name="set_rewind_prev_desc">Premotaj prije vraćanja na prethodnu pjesmu</string>
<string name="desc_play_pause">Reproduciraj ili pauziraj</string>
<string name="set_repeat_pause">Pauziraj na ponavljanje</string>
<string name="set_repeat_pause">Pauziraj pri ponavljanju</string>
<string name="set_content">Sadržaj</string>
<string name="set_save_state">Spremi stanje reprodukcije</string>
<string name="set_restore_state">Vrati stanje reprodukcije</string>
@ -249,7 +249,7 @@
<string name="lbl_wiki">Wiki</string>
<string name="fmt_list">%1$s, %2$s</string>
<string name="lbl_reset">Resetiraj</string>
<string name="set_replay_gain">ReplayGain izjednačavanje glasnoće</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_dirs_list">Mape</string>
<string name="lbl_sort_dsc">Silazno</string>
<string name="set_ui_desc">Promijenite temu i boje aplikacije</string>
@ -301,9 +301,28 @@
<string name="lbl_copied">Kopirano</string>
<string name="def_album_count">Nema albuma</string>
<string name="lbl_imported_playlist">Uvezen popis pjesama</string>
<string name="lbl_path">Staza</string>
<string name="lbl_path">Putanja</string>
<string name="lbl_demo">Demo snimka</string>
<string name="lbl_demos">Demo snimke</string>
<string name="err_import_failed">Nije bilo moguće uvesti popis pjesama iz ove datoteke</string>
<string name="err_import_failed">Nije moguće uvesti popis pjesama iz ove datoteke</string>
<string name="lbl_empty_playlist">Prazan popis pjesama</string>
<string name="lbl_author">Autor</string>
<string name="lbl_donate">Doniraj</string>
<string name="lbl_supporters">Podržavatelji</string>
<string name="lng_supporters_promo">Doniraj projektu za dodavanje tvog imena ovdje!</string>
<string name="lbl_import_playlist">Uvezi popis pjesama</string>
<string name="err_export_failed">Nije moguće izvesti popis pjesama u ovu datoteku</string>
<string name="lbl_import">Uvezi</string>
<string name="lbl_export">Izvezi</string>
<string name="lbl_export_playlist">Izvezi popis pjesama</string>
<string name="lbl_path_style">Stil putanje</string>
<string name="lbl_path_style_absolute">Absolutno</string>
<string name="lbl_path_style_relative">Relativno</string>
<string name="lbl_windows_paths">Koristi Windows kompatibilne putanje</string>
<string name="lng_playlist_imported">Popis pjesama je uvezen</string>
<string name="lng_playlist_exported">Popis pjesama je izvezen</string>
<string name="lbl_replaygain_track">Podešavanje ReplayGain pjesme</string>
<string name="lbl_replaygain_album">Podešavanje ReplayGain albuma</string>
<string name="set_remember_pause">Zapamti pauzu</string>
<string name="set_remember_pause_desc">Nastavi reprodukciju/pauziranje prilikom preskakanja ili uređivanja slijeda</string>
</resources>

View file

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="lbl_indexing">Cargante le musica</string>
<string name="lbl_retry">Retentar</string>
<string name="lbl_show_error_info">Plus</string>
<string name="lbl_grant">Conceder</string>
<string name="lbl_songs">Cantos</string>
<string name="lbl_song">Canto</string>
<string name="lbl_all_songs">Tote le cantos</string>
<string name="lbl_albums">Albumes</string>
<string name="lbl_album">Album</string>
<string name="lbl_ep">EP</string>
<string name="lbl_album_remix">Album remix</string>
<string name="lbl_eps">EPs</string>
<string name="lbl_ep_remix">EP de remix</string>
<string name="lbl_compilations">Compilationes</string>
<string name="lbl_compilation">Compilation</string>
<string name="lbl_compilation_remix">Compilatom de remix</string>
<string name="lbl_demo">Demo</string>
<string name="lbl_demos">Demos</string>
<string name="lbl_artist">Artista</string>
<string name="lbl_artists">Artistas</string>
<string name="lbl_playlist">Lista de reproduction</string>
<string name="lbl_playlists">Listas de reproduction</string>
<string name="lbl_new_playlist">Nove lista de reproduction</string>
<string name="lbl_empty_playlist">Lista de reproduction vacue</string>
<string name="lbl_import">Importar</string>
<string name="lbl_export">Exportar</string>
<string name="lbl_rename">Renominar</string>
<string name="lbl_rename_playlist">Renominar lista de reproduction</string>
<string name="lbl_delete">Deler</string>
<string name="lbl_confirm_delete_playlist">Deler le lista de reproduction?</string>
<string name="lbl_edit">Modificar</string>
<string name="lbl_filter_all">Toto</string>
<string name="lbl_filter">Filtrar</string>
<string name="lbl_name">Nomine</string>
<string name="lbl_duration">Duration</string>
<string name="lbl_song_count">Numero de cantos</string>
<string name="lbl_track">Tracia</string>
<string name="lbl_shuffle">Aleatori</string>
<string name="lbl_sort">Ordinar</string>
<string name="lbl_queue">Cauda</string>
<string name="lbl_sort_mode">Ordinar per</string>
<string name="lbl_playback">Reproduction in curso</string>
<string name="lbl_play">Reproducer</string>
<string name="lbl_play_next">Reproducer sequente</string>
<string name="lbl_queue_add">Adder al cauda</string>
<string name="lbl_add">Adder</string>
<string name="lbl_version">Version</string>
<string name="lbl_code">Codice fonte</string>
<string name="lbl_wiki">Wiki</string>
<string name="lbl_licenses">Licentias</string>
<string name="lbl_singles">Singles</string>
<string name="lbl_single">Single</string>
<string name="lbl_single_remix">Single remix</string>
<string name="lbl_mixtapes">Mixtapes</string>
<string name="lbl_mixtape">Mixtape</string>
<string name="lbl_export_playlist">Exportar le lista de reproduction</string>
<string name="lbl_date">Anno</string>
<string name="lbl_parent_detail">Vider</string>
<string name="lbl_sort_direction">Direction</string>
<string name="lbl_equalizer">Equalisator</string>
<string name="lbl_playlist_add">Adder al lista de reproduction</string>
<string name="lbl_song_detail">Vider le proprietates</string>
<string name="lbl_share">Compartir</string>
<string name="lbl_props">Proprietates del canto</string>
<string name="lbl_path">Percurso</string>
<string name="lbl_format">Formato</string>
<string name="lbl_size">Dimension</string>
<string name="lbl_sample_rate">Taxa de monstra</string>
<string name="lbl_ok">OK</string>
<string name="lbl_cancel">Cancellar</string>
<string name="lbl_save">Salveguardar</string>
<string name="lbl_reset">Reinitialisar</string>
<string name="lbl_path_style">Stylo de percurso</string>
<string name="lbl_windows_paths">Usar percursos compatibile con Windows</string>
<string name="lbl_state_saved">Stato salveguardate</string>
<string name="lbl_about">A proposito de</string>
</resources>

View file

@ -2,7 +2,7 @@
<resources>
<string name="lbl_indexer">מוזיקה נטענת</string>
<string name="lbl_indexing">מוזיקה נטענת</string>
<string name="lbl_retry">לנסות שוב</string>
<string name="lbl_retry">נסה שוב</string>
<string name="lbl_observing">ספריית המוזיקה שלך נסרקת</string>
<string name="lbl_all_songs">כל השירים</string>
<string name="lbl_albums">אלבומים</string>
@ -158,7 +158,7 @@
<string name="set_pre_amp_desc">המגבר מוחל על ההתאמה הקיימת בזמן השמעה</string>
<string name="lbl_new_playlist">רשימת השמעה חדשה</string>
<string name="lbl_playlist_add">הוספה לרשימת השמעה</string>
<string name="lbl_grant">לתת</string>
<string name="lbl_grant">הענק</string>
<string name="lbl_playlist">רשימת השמעה (פלייליסט)</string>
<string name="lbl_playlists">רשימות השמעה</string>
<string name="lbl_delete">מחיקה</string>
@ -299,4 +299,22 @@
<string name="fmt_list">%1$s, %2$s</string>
<string name="clr_lime">ליים</string>
<string name="fmt_editing">%s נערך</string>
<string name="def_album_count">אין אלבומים</string>
<string name="lbl_path_style_absolute">מחלט</string>
<string name="lbl_import">יבא</string>
<string name="lbl_windows_paths">השתמש בנתיבים המותאמים למערכת חלונות</string>
<string name="lbl_import_playlist">יבא רשימת השמעה</string>
<string name="lbl_path">נתיב</string>
<string name="lbl_export">יצא</string>
<string name="lbl_imported_playlist">רשימת השמעה מיובאת</string>
<string name="lbl_demo">דמו</string>
<string name="lbl_path_style_relative">יחסי</string>
<string name="lng_playlist_imported">רשימת השמעה יובאה</string>
<string name="lbl_demos">דמו</string>
<string name="err_import_failed">אין יכולת לייבא רשימת השמעה מהקובץ הנ”ל</string>
<string name="lbl_empty_playlist">רשימת השמעה ריקה</string>
<string name="lbl_path_style">צורת נתיב</string>
<string name="lng_playlist_exported">רשימת השמעה יוצאה</string>
<string name="lbl_export_playlist">יצא רשימת השמעה</string>
<string name="err_export_failed">אין יכולת לייצא רשימת השמעה מהקובץ הנ”ל</string>
</resources>

View file

@ -47,7 +47,7 @@
<string name="lbl_version">버전</string>
<string name="lbl_code">소스 코드</string>
<string name="lbl_licenses">라이선스</string>
<string name="lbl_author_name">Alexander Capehart가 개발</string>
<string name="lbl_author_name">Alexander Capehart</string>
<string name="lbl_library_counts">라이브러리 통계</string>
<!-- Settings namespace | Settings-related labels -->
<string name="set_root_title">설정</string>
@ -309,4 +309,19 @@
<string name="lbl_demo">데모</string>
<string name="lbl_demos">데모</string>
<string name="lbl_empty_playlist">빈 재생 목록</string>
<string name="lbl_imported_playlist">재생 목록을 가져왔습니다.</string>
<string name="lbl_author">개발자</string>
<string name="lbl_import_playlist">재생 목록 가져오기</string>
<string name="lbl_donate">후원</string>
<string name="lbl_supporters">서포터</string>
<string name="lng_supporters_promo">여기에 이름을 올리고 싶으시면 프로젝트를 후원해 주세요!</string>
<string name="lng_playlist_imported">재생 목록을 가져왔습니다.</string>
<string name="lng_playlist_exported">재생 목록을 내보냈습니다.</string>
<string name="lbl_path_style_absolute">절대</string>
<string name="lbl_path_style_relative">상대</string>
<string name="lbl_windows_paths">Windows 호환 경로 사용</string>
<string name="lbl_import">가져오기</string>
<string name="lbl_export">내보내기</string>
<string name="lbl_export_playlist">재생 목록 내보내기</string>
<string name="lbl_path_style">경로 스타일</string>
</resources>

View file

@ -4,10 +4,10 @@
<string name="lbl_all_songs">Visos dainos</string>
<string name="lbl_search">Paieška</string>
<string name="lbl_filter">Filtruoti</string>
<string name="lbl_filter_all">Visos</string>
<string name="lbl_filter_all">Visi</string>
<string name="lbl_sort">Rūšiavimas</string>
<string name="lbl_name">Pavadinimas</string>
<string name="lbl_date">Metai</string>
<string name="lbl_date">Data</string>
<string name="lbl_duration">Trukmė</string>
<string name="lbl_song_count">Dainos skaičius</string>
<string name="lbl_disc">Diskas</string>
@ -21,10 +21,10 @@
<string name="lbl_song_detail">Peržiūrėti ypatybes</string>
<string name="lbl_size">Dydis</string>
<string name="lbl_bitrate">Bitų srautas</string>
<string name="lbl_sample_rate">Mėginių ėmimo dažnis</string>
<string name="lbl_sample_rate">Skaitmeninimo dažnis</string>
<string name="set_theme_auto">Automatinis</string>
<string name="set_theme_day">Šviesus</string>
<string name="set_theme_night">Tamsus</string>
<string name="set_theme_day">Šviesi</string>
<string name="set_theme_night">Tamsi</string>
<string name="set_accent">Spalvų schema</string>
<string name="set_black_mode">Juodoji tema</string>
<string name="lbl_artists">Atlikėjai</string>
@ -34,7 +34,7 @@
<string name="lbl_play">Groti</string>
<string name="lbl_licenses">Licencijos</string>
<string name="lbl_shuffle">Maišyti</string>
<string name="lng_queue_added">Pridėtas į eilę</string>
<string name="lng_queue_added">Pridėta į eilę</string>
<string name="lbl_props">Dainų ypatybės</string>
<string name="lbl_save">Išsaugoti</string>
<string name="lbl_about">Apie</string>
@ -44,21 +44,21 @@
<string name="lbl_version">Versija</string>
<string name="set_root_title">Nustatymai</string>
<string name="set_theme">Tema</string>
<string name="set_black_mode_desc">Naudoti grynai juodą tamsią temą</string>
<string name="set_black_mode_desc">Naudoti grynai juodą tamsią temą.</string>
<string name="info_app_desc">Paprastas, racionalus Android muzikos grotuvas.</string>
<string name="lbl_indexer">Muzikos pakraunimas</string>
<string name="lbl_indexer">Muzikos pakrovimas</string>
<string name="lng_widget">Peržiūrėk ir valdyk muzikos grojimą</string>
<string name="lbl_genres">Žanrai</string>
<string name="lbl_retry">Pakartoti</string>
<string name="lbl_grant">Suteikti</string>
<string name="lbl_indexing">Kraunama muzika</string>
<string name="lng_indexing">Kraunamas tavo muzikos biblioteka…</string>
<string name="lng_indexing">Kraunama tavo muzikos biblioteka…</string>
<string name="lbl_library_counts">Bibliotekos statistika</string>
<string name="clr_pink">Rožinis</string>
<string name="lbl_album">Albumas</string>
<string name="lbl_ep">Mini albumas</string>
<string name="lbl_single">Singlas</string>
<string name="lbl_artist">Atlikėjas (-a)</string>
<string name="lbl_artist">Atlikėjas</string>
<string name="def_genre">Nežinomas žanras</string>
<string name="def_date">Nėra datos</string>
<string name="clr_red">Raudona</string>
@ -82,7 +82,7 @@
<string name="set_replay_gain_mode">ReplayGain strategija</string>
<string name="lbl_singles">Singlai</string>
<string name="lbl_ok">Gerai</string>
<string name="set_round_mode_desc">Įjungti suapvalintų kampų papildomiems UI elementams (reikia, kad albumo viršeliai būtų suapvalinti)</string>
<string name="set_round_mode_desc">Įjungti suapvalintų kampų papildomiems UI elementams (reikia, kad albumo viršeliai būtų suapvalinti).</string>
<string name="lbl_soundtrack">Garso takelis</string>
<string name="lbl_soundtracks">Garso takeliai</string>
<string name="set_audio">Garsas</string>
@ -120,56 +120,56 @@
<string name="lbl_album_live">Gyvai albumas</string>
<string name="lbl_album_remix">Remikso albumas</string>
<string name="lbl_live_group">Gyvai</string>
<string name="set_headset_autoplay_desc">Visada pradėti groti, kai ausinės yra prijungtos (gali neveikti visuose įrenginiuose)</string>
<string name="set_headset_autoplay_desc">Visada pradėti groti, kai ausinės yra prijungtos (gali neveikti visuose įrenginiuose).</string>
<string name="cdc_ogg">Ogg garsas</string>
<string name="lbl_author_name">Sukūrė Alexanderis Capehartas (angl. Alexander Capehart)</string>
<string name="set_replay_gain_mode_track">Pageidauti takelį</string>
<string name="err_no_dirs">Jokių aplankų</string>
<string name="err_bad_dir">Šis aplankas nepalaikomas</string>
<string name="lbl_author_name">Aleksandras Keiphartas (angl. Alexander Capehart)</string>
<string name="set_replay_gain_mode_track">Pageidauti takeliui</string>
<string name="err_no_dirs">Nėra aplankų</string>
<string name="err_bad_dir">Šis aplankas nepalaikomas.</string>
<string name="desc_play_pause">Groti arba pristabdyti</string>
<string name="desc_skip_next">Praleisti į kitą dainą</string>
<string name="desc_skip_prev">Praleisti į paskutinę dainą</string>
<string name="lbl_mixtape">Mikstapas</string>
<string name="lbl_mixtapes">Mikstapai</string>
<string name="set_lib_tabs">Bibliotekos skirtukai</string>
<string name="set_lib_tabs_desc">Keisti bibliotekos skirtukų matomumą ir tvarką</string>
<string name="set_lib_tabs_desc">Keisti bibliotekos skirtukų matomumą ir tvarką.</string>
<string name="set_replay_gain_mode_album">Pageidauti albumui</string>
<string name="set_replay_gain_mode_dynamic">Pageidauti albumui, jei vienas groja</string>
<string name="err_no_app">Jokią programą nerasta, kuri galėtų atlikti šią užduotį</string>
<string name="err_no_app">Programėlę nerasta, kuri galėtų atlikti šią užduotį.</string>
<string name="desc_auxio_icon">Auxio piktograma</string>
<string name="desc_song_handle">Perkelti šią dainą</string>
<string name="desc_tab_handle">Perkelti šį skirtuką</string>
<string name="err_index_failed">Muzikos įkrovimas nepavyko</string>
<string name="err_no_perms">Auxio reikia leidimo skaityti tavo muzikos biblioteką</string>
<string name="err_index_failed">Muzikos pakrovimas nepavyko.</string>
<string name="err_no_perms">Auxio reikia leidimo skaityti tavo muzikos biblioteką.</string>
<string name="fmt_disc_no">Diskas %d</string>
<string name="fmt_db_pos">+%.1f dB</string>
<string name="fmt_db_neg">-%.1f dB</string>
<string name="fmt_lib_total_duration">Bendra trukmė: %s</string>
<string name="lbl_single_live">Gyvas singlas</string>
<string name="lbl_single_live">Gyvai singlas</string>
<string name="lbl_single_remix">Remikso singlas</string>
<string name="lbl_compilations">Kompiliacijos</string>
<string name="lbl_compilation">Kompiliacija</string>
<string name="lbl_compilations">Rinkiniai</string>
<string name="lbl_compilation">Rinkinys</string>
<string name="set_keep_shuffle">Prisiminti maišymą</string>
<string name="set_keep_shuffle_desc">Palikti maišymą įjungtą, kai groja nauja daina</string>
<string name="set_keep_shuffle_desc">Palikti maišymą įjungtą, kai groja nauja daina.</string>
<string name="set_rewind_prev">Persukti prieš praleistant atgal</string>
<string name="set_rewind_prev_desc">Persukti atgal prieš praleistant į ankstesnę dainą</string>
<string name="set_repeat_pause">Pauzė ant kartojamo</string>
<string name="set_rewind_prev_desc">Persukti atgal prieš praleistant į ankstesnę dainą.</string>
<string name="set_repeat_pause">Pauzė ant kartojimo</string>
<string name="set_play_in_list_with">Kai grojant iš bibliotekos</string>
<string name="set_play_in_parent_with">Kai grojant iš elemento detalių</string>
<string name="desc_music_dir_delete">Pašalinti aplanką</string>
<string name="lbl_genre">Žanras</string>
<string name="lng_search_library">Ieškoti savo bibliotekoje…</string>
<string name="lng_search_library">Ieškok savo bibliotekoje…</string>
<string name="lbl_equalizer">Ekvalaizeris</string>
<string name="set_dirs_mode">Režimas</string>
<string name="set_observing">Automatinis įkrovimas</string>
<string name="err_no_music">Jokios muzikos nerasta</string>
<string name="set_observing">Automatinis perkrauvimas</string>
<string name="err_no_music">Muzikos nerasta.</string>
<string name="desc_exit">Sustabdyti grojimą</string>
<string name="def_track">Nėra takelio</string>
<string name="set_action_mode_next">Praleisti į kitą</string>
<string name="set_headset_autoplay">Automatinis ausinių grojimas</string>
<string name="set_action_mode_repeat">Kartojimo režimas</string>
<string name="desc_queue_bar">Atidaryti eilę</string>
<string name="desc_clear_search">Išvalyti paieškos paraišką</string>
<string name="desc_clear_search">Išvalyti paieškos užklausą</string>
<string name="set_dirs_mode_exclude_desc">Muzika <b>nebus</b> kraunama iš pridėtų aplankų, kurių tu pridėsi.</string>
<string name="set_dirs_mode_include">Įtraukti</string>
<string name="desc_remove_song">Pašalinti šią dainą</string>
@ -181,39 +181,39 @@
<string name="set_dirs_mode_exclude">Neįtraukti</string>
<string name="set_dirs_mode_include_desc">Muzika <b>bus</b> kraunama iš aplankų, kurių tu pridėsi.</string>
<string name="fmt_sample_rate">%d Hz</string>
<string name="set_observing_desc">Perkrauti muzikos biblioteką, kai ji pasikeičia (reikia nuolatinio pranešimo)</string>
<string name="set_observing_desc">Perkrauti muzikos biblioteką, kai ji pasikeičia (reikia nuolatinio pranešimo).</string>
<string name="fmt_lib_song_count">Pakrautos dainos: %d</string>
<string name="fmt_lib_genre_count">Pakrautos žanros: %d</string>
<string name="fmt_lib_album_count">Pakrauti albumai: %d</string>
<string name="fmt_lib_artist_count">Pakrauti atlikėjai: %d</string>
<string name="fmt_indexing">Kraunama tavo muzikos biblioteka… (%1$d/%2$d)</string>
<string name="desc_shuffle_all">Maišyti visas dainas</string>
<string name="set_personalize">Personalizuotas</string>
<string name="set_pre_amp_warning">Įspėjimas: keičiant išankstinį stiprintuvą į didelę teigiamą vertę, kai kuriuose garso takeliuose gali atsirasti tarpų.</string>
<string name="set_personalize">Suasmeninti</string>
<string name="set_pre_amp_warning">Įspėjimas: keičiant išankstinį stiprintuvą į didelę teigiamą reikšmę, kai kuriuose garso takeliuose gali atsirasti tarpų.</string>
<string name="desc_album_cover">Albumo viršelis %s</string>
<string name="desc_artist_image">Atlikėjo vaizdas %s</string>
<string name="def_playback">Nėra grojančio muzikos</string>
<string name="set_repeat_pause_desc">Sustabdyti, kai daina kartojasi</string>
<string name="set_repeat_pause_desc">Sustabdyti, kai daina kartojasi.</string>
<string name="set_content">Turinys</string>
<string name="set_dirs">Muzikos aplankai</string>
<string name="set_reindex">Atnaujinti muziką</string>
<string name="set_reindex_desc">Perkrauti muzikos biblioteką, naudojant talpyklos žymes, kai įmanoma</string>
<string name="set_reindex_desc">Perkrauti muzikos biblioteką, naudojant talpyklos žymes, kai įmanoma.</string>
<string name="set_bar_action">Pasirinktinis grojimo juostos veiksmas</string>
<string name="err_did_not_restore">Nepavyko atkurti būsenos</string>
<string name="err_did_not_restore">Nepavyksta atkurti būsenos.</string>
<string name="set_pre_amp">ReplayGain išankstinis stiprintuvas</string>
<string name="set_save_state">Išsaugoti grojimo būseną</string>
<string name="set_dirs_desc">Tvarkyti, kur muzika turėtų būti įkeliama iš</string>
<string name="set_dirs_desc">Tvarkyti, kur muzika turėtų būti kraunama iš.</string>
<string name="desc_genre_image">Žanro vaizdas %s</string>
<string name="desc_shuffle">Įjungti maišymą arba išjungti</string>
<string name="desc_track_number">Takelis %d</string>
<string name="desc_track_number">%d takelis</string>
<string name="desc_change_repeat">Keisti kartojimo režimą</string>
<string name="clr_indigo">Indigos</string>
<string name="fmt_bitrate">%d kbps</string>
<string name="lbl_mixes">DJ miksai</string>
<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="set_wipe_desc">Išvalyti anksčiau išsaugotą grojimo būseną (jei yra)</string>
<string name="lbl_compilation_live">Gyvai rinkinys</string>
<string name="lbl_compilation_remix">Remikso rinkinys</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>
<string name="set_separators_plus">Pliusas (+)</string>
@ -221,56 +221,56 @@
<string name="set_cover_mode">Albumų viršeliai</string>
<string name="set_cover_mode_off">Išjungta</string>
<string name="set_cover_mode_media_store">Greitis</string>
<string name="set_save_desc">Išsaugoti dabartinę grojimo būseną dabar</string>
<string name="set_save_desc">Išsaugoti dabartinę grojimo būseną dabar.</string>
<string name="set_wipe_state">Išvalyti grojimo būseną</string>
<string name="set_separators_desc">Konfigūruoti simbolius, kurie nurodo kelias žymių reikšmes</string>
<string name="set_separators_desc">Konfigūruoti simbolius, kurie nurodo kelias žymių reikšmes.</string>
<string name="set_separators_comma">Kablelis (,)</string>
<string name="set_pre_amp_without">Reguliavimas be žymų</string>
<string name="set_separators_warning">Įspėjimas: naudojant šį nustatymą, kai kurios žymos gali būti neteisingai interpretuojamos kaip turinčios kelias reikšmes. Tai galima išspręsti prieš nepageidaujamus skiriamuosius ženklus su agalinių brūkšniu (\\).</string>
<string name="set_pre_amp_without">Koregavimas be žymių</string>
<string name="set_separators_warning">Įspėjimas: naudojant šį nustatymą, kai kurios žymes gali būti neteisingai interpretuojamos kaip turinčios kelias reikšmes. Tai galima išspręsti prieš nepageidaujamus skiriamuosius ženklus su agalinių brūkšniu (\\).</string>
<string name="set_separators_semicolon">Kabliataškis (;)</string>
<string name="set_cover_mode_quality">Aukštos kokybės</string>
<string name="set_restore_state">Atkurti grojimo būseną</string>
<string name="set_exclude_non_music">Neįtraukti nemuzikinių</string>
<string name="set_exclude_non_music_desc">Ignoruoti garso failus, kurie nėra muzika, pvz., tinklalaides</string>
<string name="set_pre_amp_desc">Išankstinis stiprintuvas taikomas esamam reguliavimui grojimo metu</string>
<string name="set_pre_amp_with">Reguliavimas su žymėmis</string>
<string name="set_restore_desc">Atkurti anksčiau išsaugotą grojimo būseną (jei yra)</string>
<string name="set_exclude_non_music_desc">Ignoruoti garso failus, kurie nėra muzika, tokius kaip tinklalaides.</string>
<string name="set_pre_amp_desc">Išankstinis stiprintuvas taikomas esamam koregavimui grojimo metu.</string>
<string name="set_pre_amp_with">Koregavimas su žymėmis</string>
<string name="set_restore_desc">Atkurti anksčiau išsaugotą grojimo būseną (jei yra).</string>
<string name="set_hide_collaborators">Slėpti bendradarbius</string>
<string name="set_hide_collaborators_desc">Rodyti tik tuos atlikėjus, kurie yra tiesiogiai įrašyti į albumą (geriausiai veikia gerai pažymėtose bibliotekose)</string>
<string name="err_did_not_wipe">Nepavyko išvalyti būsenos</string>
<string name="err_did_not_save">Nepavyko išsaugoti būsenos</string>
<string name="set_hide_collaborators_desc">Rodyti tik tuos atlikėjus, kurie yra tiesiogiai įtraukti į albumą (geriausiai veikia gerai pažymėtose bibliotekose).</string>
<string name="err_did_not_wipe">Nepavyksta išvalyti būsenos.</string>
<string name="err_did_not_save">Nepavyksta išsaugoti būsenos.</string>
<plurals name="fmt_artist_count">
<item quantity="one">%d atlikėjas (-a)</item>
<item quantity="one">%d atlikėjas</item>
<item quantity="few">%d atlikėjai</item>
<item quantity="other">%d atlikėjų</item>
<item quantity="many">%d atlikėjų</item>
<item quantity="other">%d atlikėjų</item>
</plurals>
<string name="set_rescan">Perskenuoti muziką</string>
<string name="set_rescan_desc">Išvalyti žymių talpyklą ir pilnai perkrauti muzikos biblioteką (lėčiau, bet labiau išbaigta)</string>
<string name="set_rescan_desc">Išvalyti žymių talpyklą ir pilnai perkrauti muzikos biblioteką (lėčiau, bet labiau užbaigta).</string>
<string name="fmt_selected">%d pasirinkta</string>
<string name="set_play_song_from_genre">Groti iš žanro</string>
<string name="lbl_wiki">Viki</string>
<string name="fmt_list">%1$s, %2$s</string>
<string name="lbl_reset">Nustatyti iš naujo</string>
<string name="lbl_reset">Atkurti</string>
<string name="set_library">Biblioteka</string>
<string name="set_behavior">Elgesys</string>
<string name="set_ui_desc">Pakeisk programos temą ir spalvas</string>
<string name="set_content_desc">Valdyk, kaip muzika ir vaizdai įkeliami</string>
<string name="set_audio_desc">Konfigūruok garso ir grojimo elgesį</string>
<string name="set_personalize_desc">Pritaikyk UI valdiklius ir elgseną</string>
<string name="set_ui_desc">Pakeisk programėlės temą ir spalvas.</string>
<string name="set_content_desc">Valdyk, kaip muzika ir vaizdai kraunami.</string>
<string name="set_audio_desc">Konfigūruok garso ir grojimo elgesį.</string>
<string name="set_personalize_desc">Pritaikyk naudotojo sąsajos valdiklius ir elgseną.</string>
<string name="set_music">Muzika</string>
<string name="set_images">Vaizdai</string>
<string name="set_playback">Grojimas</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_dirs_list">Aplankalai</string>
<string name="set_dirs_list">Aplankai</string>
<string name="set_state">Pastovumas</string>
<string name="lbl_sort_dsc">Mažėjantis</string>
<string name="set_intelligent_sorting_desc">Teisingai surūšiuoti pavadinimus, kurie prasideda skaičiais arba žodžiais, tokiais kaip „the“ (geriausiai veikia su anglų kalbos muzika)</string>
<string name="set_intelligent_sorting_desc">Teisingai surūšiuoti pavadinimus, kurie prasideda skaičiais arba žodžiais, tokiais kaip „the“ (geriausiai veikia su anglų kalbos muzika).</string>
<string name="set_intelligent_sorting">Išmanusis rūšiavimas</string>
<string name="lbl_playlist">Grojaraštis</string>
<string name="lbl_playlists">Grojaraščiai</string>
<string name="desc_playlist_image">Grojaraščio vaizdas %s</string>
<string name="desc_new_playlist">Sukurti naują grojaraštį</string>
<string name="desc_new_playlist">Kurti naują grojaraštį</string>
<string name="lbl_new_playlist">Naujas grojaraštis</string>
<string name="lbl_playlist_add">Pridėti į grojaraštį</string>
<string name="lng_playlist_added">Pridėta į grojaraštį</string>
@ -283,7 +283,7 @@
<string name="lbl_edit">Redaguoti</string>
<string name="lbl_share">Bendrinti</string>
<string name="lng_playlist_renamed">Pervadintas grojaraštis</string>
<string name="fmt_def_playlist">Grojaraštis %d</string>
<string name="fmt_def_playlist">%d grojaraštis</string>
<string name="lng_playlist_created">Sukurtas grojaraštis</string>
<string name="lng_playlist_deleted">Ištrintas grojaraštis</string>
<string name="def_disc">Nėra disko</string>
@ -291,7 +291,7 @@
<string name="lbl_appears_on">Pasirodo</string>
<string name="lbl_song">Daina</string>
<string name="lbl_parent_detail">Peržiūrėti</string>
<string name="set_square_covers_desc">Apkarpyti visus albumų viršelius iki 1:1 kraštinių koeficiento</string>
<string name="set_square_covers_desc">Apkarpyti visus albumų viršelius iki 1:1 kraštinių koeficiento.</string>
<string name="set_square_covers">Priversti kvadratinių albumų viršelius</string>
<string name="set_play_song_by_itself">Groti dainą pačią</string>
<string name="lbl_sort_mode">Rūšiuoti pagal</string>
@ -305,4 +305,27 @@
<string name="def_album_count">Nėra albumų</string>
<string name="lbl_demo">Demo</string>
<string name="lbl_demos">Demos</string>
<string name="lbl_imported_playlist">Importuotas grojaraštis</string>
<string name="lng_playlist_imported">Importuotas grojaraštis</string>
<string name="lng_playlist_exported">Eksportuotas grojaraštis</string>
<string name="err_export_failed">Nepavyksta eksportuoti grojaraščio į šį failą</string>
<string name="lbl_replaygain_track">ReplayGain takelio koregavimas</string>
<string name="lbl_replaygain_album">ReplayGain albumo koregavimas</string>
<string name="lbl_author">Autorius</string>
<string name="lbl_donate">Aukoti</string>
<string name="lbl_supporters">Palaikytojai</string>
<string name="lng_supporters_promo">Paaukok projektui, kad tavo vardas būtų pridėtas čia!</string>
<string name="err_import_failed">Nepavyksta importuoti grojaraščio iš šio failo.</string>
<string name="lbl_empty_playlist">Tuščias grojaraštis</string>
<string name="lbl_import_playlist">Importuoti grojaraštį</string>
<string name="lbl_path">Kelias</string>
<string name="lbl_import">Importuoti</string>
<string name="lbl_export">Eksportuoti</string>
<string name="lbl_export_playlist">Eksportuoti grojaraštį</string>
<string name="lbl_path_style">Kelio stilius</string>
<string name="lbl_path_style_absolute">Absoliutinis</string>
<string name="lbl_path_style_relative">Santykinis</string>
<string name="lbl_windows_paths">Naudoti Windows suderinamus kelius</string>
<string name="set_remember_pause">Prisiminti pauzę</string>
<string name="set_remember_pause_desc">Išlieka grojimas ir (arba) pristabdomas, kai praleidžiama arba redaguojama eilė.</string>
</resources>

View file

@ -313,4 +313,13 @@
<string name="lng_playlist_exported">ਪਲੇਲਿਸਟ ਨਿਰਯਾਤ ਕੀਤੀ ਗਈ</string>
<string name="lbl_export_playlist">ਪਲੇਲਿਸਟ ਨਿਰਯਾਤ ਕਰੋ</string>
<string name="err_export_failed">ਪਲੇਲਿਸਟ ਨੂੰ ਇਸ ਫ਼ਾਈਲ ਵਿੱਚ ਨਿਰਯਾਤ ਕਰਨ ਵਿੱਚ ਅਸਮਰੱਥ</string>
<string name="lbl_import_playlist">ਪਲੇਲਿਸਟ ਇੰਪੋਰਟ ਕਰੋ</string>
<string name="lbl_replaygain_track">ਰੀਪਲੇਅਗੇਨ ਟ੍ਰੈਕ ਐਡਜਸਟਮੈਂਟ</string>
<string name="lbl_replaygain_album">ਰੀਪਲੇਗੇਨ ਐਲਬਮ ਐਡਜਸਟਮੈਂਟ</string>
<string name="lbl_author">ਲੇਖਕ</string>
<string name="lbl_donate">ਦਾਨ ਕਰੋ</string>
<string name="lbl_supporters">ਸਮਰਥਕ</string>
<string name="lng_supporters_promo">ਆਪਣਾ ਨਾਮ ਇੱਥੇ ਜੋੜਨ ਲਈ ਪ੍ਰੋਜੈਕਟ ਨੂੰ ਦਾਨ ਕਰੋ!</string>
<string name="set_remember_pause_desc">ਕਤਾਰ ਨੂੰ ਛੱਡਣ ਜਾਂ ਸੰਪਾਦਿਤ ਕਰਨ ਵੇਲੇ ਚਲਾਉਂਦੇ/ਰੋਕੇ ਰਹੋ</string>
<string name="set_remember_pause">ਵਿਰਾਮ ਯਾਦ ਰੱਖੋ</string>
</resources>

View file

@ -294,4 +294,30 @@
<string name="lbl_rename_playlist">Renomear playlist</string>
<string name="lbl_appears_on">Aparece em</string>
<string name="fmt_deletion_info">Apagar %s\? Esta ação não pode ser desfeita.</string>
<string name="lbl_demo">Demo</string>
<string name="lbl_imported_playlist">Playlist importada</string>
<string name="lbl_parent_detail">Visualizar</string>
<string name="lng_playlist_imported">Playlist importada</string>
<string name="lng_playlist_exported">Playlist exportada</string>
<string name="err_import_failed">Incapaz de importar uma playlist deste arquivo</string>
<string name="err_export_failed">Incapaz de exportar a playlist para este arquivo</string>
<string name="lbl_demos">Demos</string>
<string name="lbl_author">Autor</string>
<string name="lbl_donate">Doar</string>
<string name="lbl_supporters">Apoiadores</string>
<string name="lng_supporters_promo">Doe para o projeto para ter o seu nome adicionado aqui!</string>
<string name="set_remember_pause">Lembrar pausa</string>
<string name="set_remember_pause_desc">Manter reproduzindo/pausado quando ao pular ou editar a fila</string>
<string name="lbl_empty_playlist">Playlist vazia</string>
<string name="lbl_import_playlist">Importar playlist</string>
<string name="lbl_path">Caminho</string>
<string name="lbl_selection">Seleção</string>
<string name="lbl_export">Exportar</string>
<string name="lbl_export_playlist">Exportar playlist</string>
<string name="lbl_sort_direction">Direção</string>
<string name="lbl_path_style">Estilo de caminho</string>
<string name="lbl_path_style_absolute">Absoluto</string>
<string name="lbl_path_style_relative">Relativo</string>
<string name="lbl_import">Importar</string>
<string name="lbl_windows_paths">Usar caminhos compatíveis com Windows</string>
</resources>

View file

@ -37,7 +37,7 @@
<string name="set_accent">Esquema de cores</string>
<string name="set_audio">Áudio</string>
<string name="set_personalize">Personalizar</string>
<string name="set_keep_shuffle">Memorizar musica misturada</string>
<string name="set_keep_shuffle">Memorizar música misturada</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">Nenhuma música encontrada</string>
<!-- Description Namespace | Accessibility Strings -->

View file

@ -189,4 +189,98 @@
<string name="set_cover_mode">Coperți de album</string>
<string name="lbl_playlist_add">Adaugă către listă de redare</string>
<string name="lbl_sort_direction">Direcție</string>
<string name="def_date">Fără dată</string>
<string name="lbl_empty_playlist">Playlist gol</string>
<string name="set_separators_warning">Atenție: Folosirea acestei setări poate rezulta în unele taguri interpretate greșit ca având mai multe valori.
\nPoți rezolva asta punând un backslash (\\) înaintea caracterelor de separare nedorite.</string>
<string name="set_headset_autoplay_desc">Pornește mereu redarea când niște căști sunt conectate (s-ar putea să nu meargă pe toate dispozitivele)</string>
<string name="set_rescan">Re-scanează muzica</string>
<string name="set_rescan_desc">Șterge memoria cache cu taguri și reîncarcă biblioteca de muzică de tot (mai încet, dar mai complet)</string>
<string name="set_restore_state">Restaurează starea redării</string>
<string name="fmt_lib_song_count">Cântece încărcate %d</string>
<string name="desc_shuffle_all">Amestecă toate cântecele</string>
<string name="clr_cyan">Bleu</string>
<string name="def_playback">Nu se redă muzică</string>
<string name="fmt_deletion_info">Ștergi %s? Nu te poți răzgândi după aceea.</string>
<string name="fmt_lib_artist_count">Artiști încărcați: %d</string>
<string name="lbl_imported_playlist">Playlist importat</string>
<string name="clr_dynamic">Dinamic</string>
<plurals name="fmt_artist_count">
<item quantity="one">%d artist</item>
<item quantity="few">%d artiști</item>
<item quantity="other">%d de artiști</item>
</plurals>
<string name="set_hide_collaborators_desc">Arată doar artiști care sunt creditați direct pe albun (Funcționează mai bine pe bibloteci cu taguri puse bine)</string>
<string name="err_bad_dir">Dosarul ăsta nu e suportat</string>
<string name="desc_new_playlist">Crează un nou playlist</string>
<string name="desc_no_cover">Copertă album</string>
<string name="set_library">Bibliotecă</string>
<string name="set_separators_slash">Slash (/)</string>
<string name="desc_queue_bar">Deschide lista de așteptare</string>
<string name="set_save_desc">Salvează starea redării acum</string>
<string name="set_wipe_state">Uită starea redării</string>
<string name="desc_genre_image">Imagine gen pentru %s</string>
<string name="desc_playlist_image">Imagine playlist pentru %s</string>
<string name="def_artist">Artist necunoscut</string>
<string name="err_did_not_restore">Nu s-a pututu restaura starea</string>
<string name="err_did_not_save">Nu s-a putut salva starea</string>
<string name="lbl_show_error_info">Vezi mai mult</string>
<string name="set_separators_desc">Configurează caracterele care denotă mai multe valori de taguri</string>
<string name="set_dirs">Foldere cu muzică</string>
<string name="set_dirs_list">Foldere</string>
<string name="set_dirs_mode_exclude">Exclude</string>
<string name="set_dirs_mode_exclude_desc">Muzica <b>nu<b> va fi încărcată din dosarele pe care le adaugi aici.</b></b></string>
<string name="def_song_count">Fără cântece</string>
<string name="desc_artist_image">Imagine artist pentru %s</string>
<string name="lng_playlist_imported">Playlist importat</string>
<string name="lbl_author">Autor</string>
<string name="lbl_donate">Donează</string>
<string name="lng_playlist_exported">Playlist exportat</string>
<string name="lng_supporters_promo">Donează proiectului ca să ai numele adăugat aici!</string>
<string name="set_dirs_mode_include_desc">Muzica va fi încărcată <b>doar<b> din folderele pe care le adaugi aici.</b></b></string>
<string name="set_headset_autoplay">Redă automat la conectarea căștilor</string>
<string name="err_no_app">N-a fost găsită nicio aplicație care poate face asta</string>
<string name="fmt_editing">Se editează %s</string>
<string name="fmt_disc_no">Discul %d</string>
<string name="fmt_def_playlist">Playlist %d</string>
<string name="fmt_lib_genre_count">Genuri încărcate: %d</string>
<string name="set_playback">Redare</string>
<string name="set_repeat_pause">Pauză la repetare</string>
<string name="set_audio_desc">Configurează comportamentul sunetului și redării</string>
<string name="set_save_state">Salvează starea redării</string>
<string name="def_track">Fără track</string>
<string name="set_dirs_desc">Configurează de unde se încarcă muzica</string>
<string name="set_reindex">Reîncarcă muzica</string>
<string name="desc_album_cover">Copertă album pentru %s</string>
<string name="def_genre">Gen necunoscut</string>
<string name="def_disc">Fără disc</string>
<string name="set_music">Muzică</string>
<string name="clr_deep_purple">Mov închis</string>
<string name="set_remember_pause">Ține minte pauza</string>
<string name="set_remember_pause_desc">Ține minte pauza atunci când dai skip printre cântece</string>
<string name="desc_music_dir_delete">Elimină dosarul</string>
<string name="desc_selection_image">Imagine selecție</string>
<string name="clr_indigo">Indigo</string>
<string name="fmt_selected">%d Selectate</string>
<string name="fmt_lib_album_count">Albume încărcate %d</string>
<string name="lbl_import_playlist">Importă playlist</string>
<string name="set_dirs_mode_include">Include</string>
<string name="set_reindex_desc">Reîncarcă biblioteca cu muzică, folosind taguri din memoria cache</string>
<string name="lbl_error_info">Informații despre eroare</string>
<string name="lbl_copied">Copiat</string>
<string name="lbl_report">Raportează</string>
<string name="set_play_song_by_itself">Redă cântecul fără să facă parte din nicio listă</string>
<string name="set_repeat_pause_desc">Pune pauză atunci când un cântec se repetă</string>
<string name="set_dirs_mode">Mod</string>
<string name="err_index_failed">Încărcarea muzicii a eșuat</string>
<string name="err_no_perms">Auxio are nevoie de permisiune ca să-ți acceseze biblioteca de muzică</string>
<string name="desc_song_handle">Mută acest cântec</string>
<string name="err_no_dirs">Niciun dosar</string>
<string name="desc_shuffle">Pornește sau oprește amestecarea</string>
<string name="desc_exit">Oprește redarea</string>
<string name="desc_remove_song">Elimină acest cântec</string>
<string name="lbl_export">Exportă</string>
<string name="lbl_export_playlist">Exportă playlistul</string>
<string name="lbl_import">Importă</string>
<string name="err_import_failed">Nu se poate importa un playlist din acest fișier</string>
</resources>

View file

@ -328,4 +328,13 @@
<string name="lng_playlist_exported">Плейлист экспортирован</string>
<string name="lbl_export_playlist">Экспортировать плейлист</string>
<string name="err_export_failed">Невозможно экспортировать плейлист в этот файл</string>
<string name="lbl_import_playlist">Импортировать плейлист</string>
<string name="lbl_replaygain_album">Подстройка ReplayGain альбома</string>
<string name="lbl_replaygain_track">Подстройка ReplayGain песни</string>
<string name="lbl_author">Автор</string>
<string name="lbl_donate">Пожертвовать</string>
<string name="lbl_supporters">Сторонники</string>
<string name="lng_supporters_promo">Сделайте пожертвование проекту, чтобы ваше имя было добавлено сюда!</string>
<string name="set_remember_pause_desc">Оставлять воспроизведение/паузу во время пропуска или редактирования очереди</string>
<string name="set_remember_pause">Запоминать паузу</string>
</resources>

View file

@ -1,29 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="lbl_retry">Försök igen</string>
<string name="lbl_indexer">Musik laddar</string>
<string name="lbl_indexing">Laddar musik</string>
<string name="lbl_indexer">Läser in musik</string>
<string name="lbl_indexing">Läser in musik</string>
<string name="lbl_all_songs">Alla spår</string>
<string name="lbl_albums">Album</string>
<string name="lbl_album">Albumet</string>
<string name="lbl_album_remix">Remix-album</string>
<string name="lbl_album">Album</string>
<string name="lbl_album_remix">Remixskiva</string>
<string name="lbl_eps">EP</string>
<string name="lbl_ep">EP</string>
<string name="lbl_ep_live">Live-EP</string>
<string name="lbl_ep_live">Live EP</string>
<string name="lbl_ep_remix">Remix-EP</string>
<string name="lbl_singles">Singlar</string>
<string name="lbl_single_remix">Remix-singel</string>
<string name="lbl_compilation">Sammanställning</string>
<string name="lbl_compilation_remix">Remix-sammanställning</string>
<string name="lbl_soundtracks">Ljudspår</string>
<string name="lbl_soundtrack">Ljudspår</string>
<string name="lbl_soundtrack">Soundtrack</string>
<string name="lbl_mixtapes">Blandband</string>
<string name="lbl_mixes">DJ-mixar</string>
<string name="lbl_mixes">DJ-Mixar</string>
<string name="lbl_live_group">Live</string>
<string name="lbl_remix_group">Remixar</string>
<string name="lbl_appears_on">Framträder på</string>
<string name="lbl_artist">Konstnär</string>
<string name="lbl_artists">Konstnärer</string>
<string name="lbl_artist">Artist</string>
<string name="lbl_artists">Artister</string>
<string name="lbl_genres">Genrer</string>
<string name="lbl_playlist">Spellista</string>
<string name="lbl_playlists">Spellistor</string>
@ -47,11 +47,11 @@
<string name="lbl_queue"></string>
<string name="lbl_play_next">Spela nästa</string>
<string name="lbl_playlist_add">Lägg till spellista</string>
<string name="lbl_artist_details">Gå till konstnär</string>
<string name="lbl_artist_details">Gå till artist</string>
<string name="lbl_album_details">Gå till album</string>
<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_props">Låtegenskaper</string>
<string name="lbl_format">Format</string>
<string name="lbl_size">Storlek</string>
<string name="lbl_sample_rate">Samplingsfrekvens</string>
@ -60,34 +60,34 @@
<string name="lbl_ok">Okej</string>
<string name="lbl_cancel">Avbryt</string>
<string name="lbl_save">Spara</string>
<string name="lbl_state_restored">Tillstånd återstallde</string>
<string name="lbl_state_restored">Tillstånd återställt</string>
<string name="lbl_about">Om</string>
<string name="lbl_code">Källkod</string>
<string name="lbl_wiki">Wiki</string>
<string name="lbl_licenses">Licenser</string>
<string name="lng_widget">Visa och kontrollera musikuppspelning</string>
<string name="lng_indexing">Laddar ditt musikbibliotek…</string>
<string name="lng_observing">Overvåker ditt musikbibliotek för ändringar</string>
<string name="lng_observing">Övervakar ändringar i ditt musikbibliotek</string>
<string name="lng_queue_added">Tillagd i kö</string>
<string name="lng_playlist_created">Spellista skapade</string>
<string name="lng_playlist_created">Spellista skapad</string>
<string name="lng_playlist_added">Tillagd till spellista</string>
<string name="lng_search_library">Sök i ditt musikbibliotek…</string>
<string name="set_root_title">Inställningar</string>
<string name="set_ui">Utseende</string>
<string name="set_ui_desc">Ändra tema och färger på appen</string>
<string name="set_ui_desc">Ändra färger och tema</string>
<string name="set_theme_auto">Automatisk</string>
<string name="set_theme_day">Ljust</string>
<string name="set_black_mode">Svart tema</string>
<string name="set_round_mode">Rundläge</string>
<string name="set_round_mode">Runt läge</string>
<string name="lbl_grant">Bevilja</string>
<string name="info_app_desc">En enkel, rationell musikspelare för Android.</string>
<string name="lbl_observing">Övervakar musikbiblioteket</string>
<string name="lbl_observing">Övervakar musikbibliotek</string>
<string name="lbl_songs">Spår</string>
<string name="lbl_album_live">Live-album</string>
<string name="lbl_album_live">Liveskiva</string>
<string name="lbl_delete">Ta bort</string>
<string name="lbl_compilation_live">Live-sammanställning</string>
<string name="lbl_single">Singel</string>
<string name="lbl_single_live">Live-singel</string>
<string name="lbl_single_live">Live singel</string>
<string name="lbl_compilations">Sammanställningar</string>
<string name="lbl_mixtape">Blandband</string>
<string name="lbl_mix">DJ-mix</string>
@ -99,33 +99,33 @@
<string name="lbl_sort">Sortera</string>
<string name="lbl_queue_add">Lägg till kö</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>
<string name="lbl_state_wiped">Tillstånd togs bort</string>
<string name="lbl_bitrate">Överföringskapacitet</string>
<string name="lbl_reset">Återställ</string>
<string name="lbl_state_saved">Tillstånd sparat</string>
<string name="lbl_version">Version</string>
<string name="lbl_library_counts">Statistik över beroende</string>
<string name="lng_playlist_renamed">Byt namn av spellista</string>
<string name="lbl_library_counts">Bibliotekstatistik</string>
<string name="lng_playlist_renamed">Bytt namn på spellista</string>
<string name="lng_playlist_deleted">Spellista tog bort</string>
<string name="lbl_author_name">Utvecklad av Alexander Capeheart</string>
<string name="lbl_author_name">Alexander Capeheart</string>
<string name="set_theme">Tema</string>
<string name="set_theme_night">Mörkt</string>
<string name="set_accent">Färgschema</string>
<string name="set_black_mode_desc">Använda rent svart för det mörka temat</string>
<string name="set_black_mode_desc">Använd ren svart till det mörka temat</string>
<string name="set_round_mode_desc">Aktivera rundade hörn på ytterligare element i användargränssnittet (kräver att albumomslag är rundade)</string>
<string name="set_personalize">Anpassa</string>
<string name="set_lib_tabs_desc">Ändra synlighet och ordningsföljd av bibliotekflikar</string>
<string name="set_lib_tabs_desc">Ändra synlighet och ordningsföljd bibliotekflikar</string>
<string name="set_bar_action">Anpassad åtgärd för uppspelningsfält</string>
<string name="set_notif_action">Anpassad aviseringsåtgärd</string>
<string name="set_action_mode_next">Hoppa till nästa</string>
<string name="set_action_mode_repeat">Upprepningsmodus</string>
<string name="set_behavior">Beteende</string>
<string name="set_play_in_parent_with">När spelar från artikeluppgifter</string>
<string name="set_play_in_parent_with">Vid uppspelning baserat på objektuppgifter</string>
<string name="set_play_song_from_genre">Spela från genre</string>
<string name="set_keep_shuffle">Komma ihåg blandningsstatus</string>
<string name="set_keep_shuffle">Kom ihåg blanda-status</string>
<string name="set_keep_shuffle_desc">Behåll blandning på när en ny låt spelas</string>
<string name="set_content">Kontent</string>
<string name="set_content_desc">Kontrollera hur musik och bilar laddas</string>
<string name="set_content">Innehåll</string>
<string name="set_content_desc">Kontrollera hur musik och bilder laddas in</string>
<string name="set_music">Musik</string>
<string name="set_observing">Automatisk omladdning</string>
<string name="set_exclude_non_music">Inkludera bara musik</string>
@ -134,33 +134,33 @@
<string name="set_separators_plus">Plus (+)</string>
<string name="set_intelligent_sorting">Intelligent sortering</string>
<string name="set_intelligent_sorting_desc">Sorterar namn som börjar med siffror eller ord som \"the\" korrekt (fungerar bäst med engelskspråkig music)</string>
<string name="set_hide_collaborators">Dölj medarbetare</string>
<string name="set_hide_collaborators">Dölj medverkande</string>
<string name="set_display">Skärm</string>
<string name="set_lib_tabs">Bibliotekflikar</string>
<string name="set_play_in_list_with">När spelar från biblioteket</string>
<string name="set_play_song_none">Spela från visad artikel</string>
<string name="set_play_in_list_with">Vid uppspelning från biblioteket</string>
<string name="set_play_song_none">Spela från visat objekt</string>
<string name="set_play_song_from_all">Spela från alla låtar</string>
<string name="set_play_song_from_artist">Spela från konstnär</string>
<string name="set_play_song_from_artist">Spela från artist</string>
<string name="set_play_song_from_album">Spela från album</string>
<string name="set_separators_semicolon">Semikolon (;)</string>
<string name="set_observing_desc">Ladda om musikbiblioteket när det ändras (kräver permanent meddelande)</string>
<string name="set_separators_comma">Komma (,)</string>
<string name="set_separators_slash">Snedstreck (/)</string>
<string name="set_separators_desc">Konfigurera tecken som separerar flera värden i taggar</string>
<string name="set_separators_warning">Advarsel: Denna inställning kan leda till att vissa taggar separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett backslash (\\).</string>
<string name="set_separators_warning">Varning: Denna inställning kan leda till att vissa taggar separeras felaktigt. För att åtgärda detta, prefixa oönskade separatortecken med ett backslash (\\).</string>
<string name="set_personalize_desc">Anpassa UI-kontroller och beteende</string>
<string name="set_cover_mode_off">Av</string>
<string name="set_headset_autoplay">Hörlurar-autouppspelning</string>
<string name="set_headset_autoplay">Autouppspelning med hörlurar</string>
<string name="set_repeat_pause_desc">Pausa när en låt upprepas</string>
<string name="set_dirs_mode_exclude_desc">Musik laddas <b>inte</b> från mapparna som ni lägger till.</string>
<string name="desc_queue_bar">Öppna kö</string>
<string name="desc_queue_bar">Öppna kön</string>
<string name="clr_dynamic">Dynamisk</string>
<string name="fmt_lib_artist_count">%d konstnärer som laddats</string>
<string name="fmt_lib_artist_count">Inlästa artister: %d</string>
<plurals name="fmt_artist_count">
<item quantity="one">%d konstnär</item>
<item quantity="other">%d konstnärer</item>
<item quantity="one">%d artist</item>
<item quantity="other">%d artister</item>
</plurals>
<string name="set_images">Bildar</string>
<string name="set_images">Bilder</string>
<string name="set_audio">Ljud</string>
<string name="set_audio_desc">Konfigurera ljud- och uppspelningsbeteende</string>
<string name="set_rewind_prev">Spola tillbaka innan spår hoppar tillbaka</string>
@ -168,23 +168,23 @@
<string name="set_wipe_desc">Rensa det tidigare sparade uppspelningsläget om det finns</string>
<string name="set_restore_state">Återställ uppspelningsläge</string>
<string name="fmt_db_neg">-%.1f dB</string>
<string name="fmt_deletion_info">Radera %s\? Detta kan inte ångras.</string>
<string name="fmt_deletion_info">Ta bort %s? Detta kan inte ångras.</string>
<string name="set_hide_collaborators_desc">Endast visa artister som är direkt krediterade på ett album (funkar bäst på välmärkta bibliotek)</string>
<string name="set_cover_mode">Albumomslag</string>
<string name="set_cover_mode_media_store">Snabbt</string>
<string name="set_library">Bibliotek</string>
<string name="set_dirs_mode_include">Inkludera</string>
<string name="set_reindex">Uppdatera musik</string>
<string name="set_reindex_desc">Ladda musikbiblioteket om och använd cachad taggar när det är möjligt</string>
<string name="set_state">Uthållighet</string>
<string name="set_reindex_desc">Läs in musik på nytt, vid möjlighet med användning av cachade taggar</string>
<string name="set_state">Persistens</string>
<string name="set_wipe_state">Rensa uppspelningsläge</string>
<string name="set_restore_desc">Återställ det tidigare lagrade uppspelningsläget om det finns</string>
<string name="err_did_not_save">Misslyckades att spara uppspelningsläget</string>
<string name="desc_shuffle_all">Blanda alla spår</string>
<string name="desc_shuffle_all">Blanda alla låtar</string>
<string name="desc_clear_search">Rensa sökfrågan</string>
<string name="desc_music_dir_delete">Radera mappen</string>
<string name="desc_music_dir_delete">Ta bort mapp</string>
<string name="desc_genre_image">Genrebild för %s</string>
<string name="desc_playlist_image">Spellistabild för %s</string>
<string name="desc_playlist_image">Bild spellista för %s</string>
<string name="cdc_mp3">MPEG-1-ljud</string>
<string name="cdc_mp4">MPEG-4-ljud</string>
<string name="cdc_ogg">OGG-ljud</string>
@ -204,84 +204,84 @@
<string name="clr_orange">Orange</string>
<string name="clr_brown">Brun</string>
<string name="set_headset_autoplay_desc">Alltid börja uppspelning när hörlurar kopplas till (kanske inte fungerar på alla enheter)</string>
<string name="set_repeat_pause">Pausa vid upprepa</string>
<string name="set_pre_amp">ReplayGain förförstärkare</string>
<string name="set_repeat_pause">Pausa vid upprepning</string>
<string name="set_pre_amp">ReplayGain försteg</string>
<string name="set_pre_amp_without">Justering utan taggar</string>
<string name="set_dirs">Musikmappar</string>
<string name="set_pre_amp_warning">Varning: Om man ändrar förförstärkaren till ett högt positivt värde kan det leda till toppning på vissa ljudspår.</string>
<string name="set_dirs_desc">Hantera var musik bör laddas in från</string>
<string name="set_dirs_desc">Hantera vart musik läses in ifrån</string>
<string name="set_dirs_list">Mappar</string>
<string name="set_dirs_mode">Modus</string>
<string name="set_dirs_mode_exclude">Utesluta</string>
<string name="set_dirs_mode_include_desc">Musik laddas <b>endast</b> från mapparna som ni lägger till.</string>
<string name="set_save_desc">Spara det aktuella uppspelningsläget</string>
<string name="set_rescan">Skanna musik om</string>
<string name="set_save_desc">Spara aktuellt uppspelningsläge</string>
<string name="set_rescan">Skanna om musik</string>
<string name="set_rescan_desc">Rensa tagbiblioteket och ladda komplett om musikbiblioteket (långsammare, men mer komplett)</string>
<string name="err_no_music">Ingen musik på gång</string>
<string name="err_index_failed">Laddning av musik misslyckades</string>
<string name="err_no_perms">Auxio behöver tillstånd för att läsa ditt musikbibliotek</string>
<string name="err_no_app">Ingen app på gång som kan hantera denna uppgift</string>
<string name="err_index_failed">Läsa in musik misslyckades</string>
<string name="err_no_perms">Auxio måste ges behörighet för att läsa in ditt musikbibliotek</string>
<string name="err_no_app">Ingen lämplig app kunde hittas</string>
<string name="err_bad_dir">Denna mapp stöds inte</string>
<string name="err_did_not_restore">Misslyckades att återställa uppspelningsläget</string>
<string name="desc_track_number">Spår %d</string>
<string name="desc_play_pause">Spela eller pausa</string>
<string name="desc_song_handle">Flytta detta spår</string>
<string name="def_artist">Okänd konstnär</string>
<string name="def_artist">Okänd artist</string>
<string name="def_genre">Okänd genre</string>
<string name="cdc_aac">Avancerad audio-koding (AAC)</string>
<string name="fmt_selected">%d utvalda</string>
<string name="cdc_aac">Avancerad audio-kodning (AAC)</string>
<string name="fmt_selected">%d valda</string>
<string name="fmt_def_playlist">Spellista %d</string>
<string name="fmt_db_pos">+%.1f dB</string>
<plurals name="fmt_song_count">
<item quantity="one">%d spår</item>
<item quantity="other">%d spår</item>
<item quantity="one">%d låt</item>
<item quantity="other">%d låtar</item>
</plurals>
<plurals name="fmt_album_count">
<item quantity="one">%d album</item>
<item quantity="other">%d album</item>
</plurals>
<string name="fmt_lib_song_count">%d spår som laddats</string>
<string name="fmt_lib_total_duration">Total längd: %s</string>
<string name="fmt_lib_song_count">Inlästa låtar: %d</string>
<string name="fmt_lib_total_duration">Total spårlängd: %s</string>
<string name="lbl_copied">Kopierade</string>
<string name="lbl_selection">Urval</string>
<string name="lbl_error_info">Felinformation</string>
<string name="lbl_report">Rapportera</string>
<string name="def_date">Ingen datum</string>
<string name="def_disc">Ingen disk</string>
<string name="def_date">Inget datum</string>
<string name="def_disc">Ingen skiva</string>
<string name="def_track">Inget spår</string>
<string name="def_song_count">Inga spår</string>
<string name="clr_purple">Lilla</string>
<string name="def_song_count">Inga låtar</string>
<string name="clr_purple">Lila</string>
<string name="fmt_bitrate">%d kbps</string>
<string name="fmt_sample_rate">%d Hz</string>
<string name="fmt_lib_album_count">%d album som laddats</string>
<string name="fmt_lib_genre_count">%d genrer som laddats</string>
<string name="set_play_song_by_itself">Spela upp låten själv</string>
<string name="fmt_lib_album_count">Inlästa album: %d</string>
<string name="fmt_lib_genre_count">Inlästa genrer: %d</string>
<string name="set_play_song_by_itself">Spela endast vald låt</string>
<string name="set_cover_mode_quality">Hög kvalitet</string>
<string name="set_square_covers">Tvinga fyrkantiga skivomslag</string>
<string name="set_square_covers_desc">Beskär alla albumomslag till en 1:1 sidförhållande</string>
<string name="set_square_covers_desc">Beskär alla albumomslag till ett 1:1 sidförhållande</string>
<string name="set_rewind_prev_desc">Spola tillbaka innan att hoppa till föregående låt</string>
<string name="set_pre_amp_with">Justering med taggar</string>
<string name="err_no_dirs">Inga mappar</string>
<string name="err_did_not_wipe">Misslyckades att rensa uppspelningsläget</string>
<string name="desc_new_playlist">Skapa en ny spellista</string>
<string name="desc_exit">Stoppa uppspelning</string>
<string name="desc_remove_song">Radera detta spår</string>
<string name="desc_remove_song">Ta bort låt</string>
<string name="desc_auxio_icon">Auxio-ikon</string>
<string name="desc_tab_handle">Flytta denna flik</string>
<string name="desc_tab_handle">Flytta flik</string>
<string name="desc_no_cover">Albumomslag</string>
<string name="desc_selection_image">Urvalbild</string>
<string name="desc_selection_image">Urvalsbild</string>
<string name="clr_deep_purple">Mörklila</string>
<string name="clr_indigo">Indigo</string>
<string name="fmt_disc_no">Disk %d</string>
<string name="fmt_disc_no">Skiva %d</string>
<string name="set_save_state">Spara uppspelningsläge</string>
<string name="desc_skip_next">Hoppa till nästa spår</string>
<string name="desc_skip_prev">Hoppa till sista spår</string>
<string name="desc_skip_next">Hoppa till nästa låt</string>
<string name="desc_skip_prev">Hoppa till sista låt</string>
<string name="desc_change_repeat">Ändra upprepningsläge</string>
<string name="desc_shuffle">Slå på eller av blandningen</string>
<string name="desc_shuffle">Blanda På/Av</string>
<string name="desc_album_cover">Albumomslag för %s</string>
<string name="desc_artist_image">Konstnärbild för %s</string>
<string name="desc_artist_image">Artistbild för %s</string>
<string name="def_playback">Ingen musik spelas</string>
<string name="cdc_flac">Fritt tapsfritt ljudkodek (FLAC)</string>
<string name="cdc_flac">Fri förlustfri ljudkodek (FLAC)</string>
<string name="clr_pink">Rosa</string>
<string name="fmt_indexing">Laddar ditt musikbibliotek… (%1$d/%2$d)</string>
<string name="set_separators_and">Ampersand (&amp;)</string>
@ -291,4 +291,35 @@
<string name="set_replay_gain_mode_dynamic">Föredra album om ett album spelar</string>
<string name="set_pre_amp_desc">Förförstarkning användas för befintliga justeringar vid uppspelning</string>
<string name="clr_red">Röd</string>
<string name="lbl_show_error_info">Visa mera</string>
<string name="lbl_song">Låt</string>
<string name="lbl_imported_playlist">Importerad spellista</string>
<string name="err_import_failed">Kunde inte importera spellista från denna fil</string>
<string name="lbl_sort_mode">Lista efter</string>
<string name="lbl_sort_direction">Riktning</string>
<string name="lbl_demo">Demo</string>
<string name="lbl_demos">Demos</string>
<string name="lbl_author">Upphovsperson</string>
<string name="lng_playlist_imported">Spellistan har importerats</string>
<string name="lng_playlist_exported">Spellistan har exporterats</string>
<string name="lng_supporters_promo">Skänk projektet ett bidrag så lägger vi till ditt namn här!</string>
<string name="set_remember_pause">Kom ihåg pausat läge</string>
<string name="set_remember_pause_desc">Fortsätt uppspelning/pausat läge vid spårbyte och listredigering</string>
<string name="err_export_failed">Kunde inte importera spellistan till denna fil</string>
<string name="lbl_empty_playlist">Tom spellista</string>
<string name="lbl_import_playlist">Importera spellista</string>
<string name="lbl_parent_detail">Vy</string>
<string name="lbl_path">Sökväg</string>
<string name="lbl_replaygain_track">ReplayGain Spårbaserad Volymjustering</string>
<string name="lbl_replaygain_album">ReplayGain Albumbaserad Volymjustering</string>
<string name="lbl_donate">Donera</string>
<string name="lbl_supporters">Bidragsgivare</string>
<string name="lbl_import">Importera</string>
<string name="lbl_export">Exportera</string>
<string name="lbl_export_playlist">Exportera spellista</string>
<string name="lbl_path_style">Sökvägsform</string>
<string name="lbl_path_style_absolute">Absolut</string>
<string name="lbl_path_style_relative">Relativ</string>
<string name="lbl_windows_paths">Använd Windowskompatibla sökvägar</string>
<string name="def_album_count">Inga album</string>
</resources>

View file

@ -325,4 +325,13 @@
<string name="lng_playlist_exported">Список відтворення експортовано</string>
<string name="lbl_export_playlist">Експортувати список відтворення</string>
<string name="err_export_failed">Неможливо експортувати список відтворення в цей файл</string>
<string name="lbl_import_playlist">Імпортувати список відтворення</string>
<string name="lbl_donate">Пожертвувати</string>
<string name="lbl_supporters">Прибічники</string>
<string name="lbl_replaygain_track">Підлаштування ReplayGain пісні</string>
<string name="lbl_replaygain_album">Підлаштування ReplayGain альбому</string>
<string name="lng_supporters_promo">Пожертвуйте на проєкт, щоб ваше ім\'я було додано сюди!</string>
<string name="lbl_author">Автор</string>
<string name="set_remember_pause_desc">Залишати відтворення/паузу під час пропуску або редагування черги</string>
<string name="set_remember_pause">Запам\'ятовувати паузу</string>
</resources>

View file

@ -319,4 +319,13 @@
<string name="err_export_failed">无法将播放列表导出到此文件</string>
<string name="lng_playlist_imported">导入了播放列表</string>
<string name="lng_playlist_exported">导出了播放列表</string>
<string name="lbl_import_playlist">导入播放列表</string>
<string name="lbl_replaygain_track">回放增益曲目调整</string>
<string name="lbl_supporters">支持者</string>
<string name="lbl_replaygain_album">回放增益专辑调整</string>
<string name="lbl_author">作者</string>
<string name="lbl_donate">捐赠</string>
<string name="lng_supporters_promo">要在此添加您的名字请给项目捐款!</string>
<string name="set_remember_pause_desc">跳过或编辑队列时保留播放/暂停状态</string>
<string name="set_remember_pause">记住暂停状态</string>
</resources>

View file

@ -15,4 +15,7 @@
<string name="cdc_vorbis">Vorbis</string>
<string name="cdc_opus">Opus</string>
<string name="cdc_wav">Microsoft WAVE</string>
<!-- Supporter Namespace | Sponsor usernames -->
<string name="sup_yrliet">yrliet</string>
</resources>

View file

@ -32,9 +32,7 @@
<string name="set_key_keep_shuffle" translatable="false">KEY_KEEP_SHUFFLE</string>
<string name="set_key_rewind_prev" translatable="false">KEY_PREV_REWIND</string>
<string name="set_key_repeat_pause" translatable="false">KEY_LOOP_PAUSE</string>
<string name="set_key_save_state" translatable="false">auxio_save_state</string>
<string name="set_key_wipe_state" translatable="false">auxio_wipe_state</string>
<string name="set_key_restore_state" translatable="false">auxio_restore_state</string>
<string name="set_key_remember_pause" translatable="false">auxio_remember_pause</string>
<string name="set_key_home_tabs" translatable="false">auxio_home_tabs</string>
<string name="set_key_hide_collaborators" translatable="false">auxio_hide_collaborators</string>

View file

@ -288,6 +288,8 @@
<string name="set_rewind_prev_desc">Rewind before skipping to the previous song</string>
<string name="set_repeat_pause">Pause on repeat</string>
<string name="set_repeat_pause_desc">Pause when a song repeats</string>
<string name="set_remember_pause">Remember pause</string>
<string name="set_remember_pause_desc">Remain playing/paused when skipping or editing queue</string>
<string name="set_replay_gain">ReplayGain</string>
<string name="set_replay_gain_mode">ReplayGain strategy</string>
<string name="set_replay_gain_mode_track">Prefer track</string>

View file

@ -4,10 +4,10 @@
android:initialLayout="@layout/widget_default"
android:minWidth="@dimen/widget_def_width"
android:minHeight="@dimen/widget_def_height"
android:minResizeHeight="0dp"
android:minResizeWidth="@dimen/widget_def_width"
android:minResizeHeight="@dimen/widget_def_height"
android:previewImage="@drawable/ui_widget_preview"
android:previewLayout="@layout/widget_small"
android:previewLayout="@layout/widget_docked_thin"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"

View file

@ -21,6 +21,12 @@
app:summary="@string/set_repeat_pause_desc"
app:title="@string/set_repeat_pause" />
<SwitchPreferenceCompat
app:defaultValue="false"
app:key="@string/set_key_remember_pause"
app:summary="@string/set_remember_pause_desc"
app:title="@string/set_remember_pause" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/set_replay_gain">

View file

@ -45,22 +45,4 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/set_state">
<Preference
app:key="@string/set_key_save_state"
app:summary="@string/set_save_desc"
app:title="@string/set_save_state" />
<Preference
app:key="@string/set_key_wipe_state"
app:summary="@string/set_wipe_desc"
app:title="@string/set_wipe_state" />
<Preference
app:key="@string/set_key_restore_state"
app:summary="@string/set_restore_desc"
app:title="@string/set_restore_state" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -3,8 +3,8 @@
android:initialLayout="@layout/widget_default"
android:minWidth="@dimen/widget_def_width"
android:minHeight="@dimen/widget_def_height"
android:minResizeHeight="0dp"
android:minResizeWidth="@dimen/widget_def_width"
android:minResizeHeight="@dimen/widget_def_height"
android:previewImage="@drawable/ui_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="0"

View file

@ -0,0 +1,2 @@
Auxio 3.4.0 adds gapless playback and new widget designs, alongside a variety of fixes.
For more information, see https://github.com/OxygenCobalt/Auxio/releases/tag/v3.4.0

View file

@ -12,6 +12,7 @@ precise/original dates, sort tags, and more
- SD Card-aware folder management
- Reliable playlisting functionality
- Playback state persistence
- Automatic gapless playback
- Full ReplayGain support (On MP3, FLAC, OGG, OPUS, and MP4 files)
- External equalizer support (ex. Wavelet)
- Edge-to-edge

View file

@ -20,4 +20,4 @@ precizni/izvorni datumi, sortiranje oznaka i više
- Automatska reprodukcija slušalica
- Elegantni widgeti koji se automatski prilagođavaju njihovoj veličini
- Potpuno privatno i izvan mreže
- Nema zaobljenih naslovnica albuma (Osim ako ih ne želite. Onda možete.)
- Bez zaobljenih naslovnica albuma (zadano)