mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-05-31 07:39:33 +03:00
Merge branch '470' into 'master'
4.7.0 Release canditate See merge request ultrasonic/ultrasonic!1091
This commit is contained in:
commit
b87203bfe4
@ -138,7 +138,7 @@ RoboTest:
|
||||
script:
|
||||
- curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
|
||||
- gcloud auth activate-service-account --key-file .secure_files/firebase-key.json
|
||||
- gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape --device model=Pixel5,version=30,locale=zh,orientation=portrait
|
||||
- gcloud firebase test android run --project ultrasonic-61089 --type robo --app ultrasonic-release/${PACKAGE_APK} --robo-directives click:button1= --device model=Nexus6,version=21,locale=en,orientation=portrait --device model=Pixel3,version=28,locale=fr,orientation=landscape
|
||||
rules:
|
||||
# Run when releasing a new tag
|
||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
||||
|
13
build.gradle
13
build.gradle
@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
buildscript {
|
||||
apply from: 'gradle/versions.gradle'
|
||||
@ -10,6 +12,7 @@ buildscript {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
maven { url "https://plugins.gradle.org/m2/" }
|
||||
}
|
||||
dependencies {
|
||||
@ -26,21 +29,29 @@ allprojects {
|
||||
buildscript {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
google()
|
||||
}
|
||||
|
||||
// Set Kotlin JVM target to the same for all subprojects
|
||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||
tasks.withType(KotlinCompile).configureEach {
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile).tap {
|
||||
configureEach {
|
||||
options.compilerArgs.add("-Xlint:deprecation")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wrapper {
|
||||
|
@ -1,11 +1,14 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
|
||||
apply from: bootstrap.androidModule
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
dependencies {
|
||||
implementation libs.core
|
||||
implementation libs.roomRuntime
|
||||
implementation libs.roomKtx
|
||||
kapt libs.room
|
||||
ksp libs.room
|
||||
}
|
||||
|
||||
android {
|
||||
|
@ -1,3 +1,7 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
|
||||
apply from: bootstrap.kotlinModule
|
||||
|
||||
dependencies {
|
||||
|
9
fastlane/metadata/android/en-US/changelogs/130.txt
Normal file
9
fastlane/metadata/android/en-US/changelogs/130.txt
Normal file
@ -0,0 +1,9 @@
|
||||
### Features
|
||||
- Added custom buttons for shuffling the current queue and setting repeat mode (Android Auto)
|
||||
- Properly handling nested directory structures (Android Auto)
|
||||
- Add a toast when adding tracks to the playlist
|
||||
- Allow pinning when offline
|
||||
|
||||
### Dependencies
|
||||
- Update koin
|
||||
- Update media3 to v1.1.0
|
@ -3,13 +3,13 @@
|
||||
gradle = "8.1.1"
|
||||
|
||||
navigation = "2.6.0"
|
||||
gradlePlugin = "8.0.2"
|
||||
gradlePlugin = "8.1.0"
|
||||
androidxcore = "1.10.1"
|
||||
ktlint = "0.43.2"
|
||||
ktlintGradle = "11.4.2"
|
||||
ktlintGradle = "11.5.0"
|
||||
detekt = "1.23.0"
|
||||
preferences = "1.2.0"
|
||||
media3 = "1.0.2"
|
||||
preferences = "1.2.1"
|
||||
media3 = "1.1.0"
|
||||
|
||||
androidSupport = "1.6.0"
|
||||
materialDesign = "1.9.0"
|
||||
@ -17,7 +17,8 @@ constraintLayout = "2.1.4"
|
||||
multidex = "2.0.1"
|
||||
room = "2.5.2"
|
||||
kotlin = "1.8.22"
|
||||
kotlinxCoroutines = "1.7.1"
|
||||
ksp = "1.8.22-1.0.11"
|
||||
kotlinxCoroutines = "1.7.3"
|
||||
viewModelKtx = "2.6.1"
|
||||
swipeRefresh = "1.1.0"
|
||||
|
||||
@ -25,11 +26,11 @@ retrofit = "2.9.0"
|
||||
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
||||
jackson = "2.13.5"
|
||||
okhttp = "4.11.0"
|
||||
koin = "3.3.2"
|
||||
koin = "3.4.3"
|
||||
picasso = "2.8"
|
||||
|
||||
junit4 = "4.13.2"
|
||||
junit5 = "5.9.3"
|
||||
junit5 = "5.10.0"
|
||||
mockito = "5.4.0"
|
||||
mockitoKotlin = "5.0.0"
|
||||
kluent = "1.73"
|
||||
@ -100,3 +101,6 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio
|
||||
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
|
||||
[plugins]
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -4,7 +4,7 @@
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
apply plugin: 'org.jetbrains.kotlin.kapt'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
|
||||
android {
|
||||
compileSdkVersion versions.compileSdk
|
||||
|
@ -2,7 +2,7 @@
|
||||
* This module provides a base for for pure kotlin modules
|
||||
*/
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'org.jetbrains.kotlin.kapt'
|
||||
apply plugin: 'com.google.devtools.ksp'
|
||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||
|
||||
sourceSets {
|
||||
@ -12,7 +12,6 @@ sourceSets {
|
||||
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
api libs.kotlinStdlib
|
||||
|
||||
|
5
gradlew
vendored
5
gradlew
vendored
@ -130,10 +130,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
|
@ -1,6 +1,9 @@
|
||||
plugins {
|
||||
alias libs.plugins.ksp
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'org.jetbrains.kotlin.android'
|
||||
apply plugin: 'org.jetbrains.kotlin.kapt'
|
||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||
apply from: "../gradle_scripts/code_quality.gradle"
|
||||
|
||||
@ -9,8 +12,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.moire.ultrasonic"
|
||||
versionCode 126
|
||||
versionName "4.6.3"
|
||||
versionCode 128
|
||||
versionName "4.7.0"
|
||||
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.targetSdk
|
||||
@ -64,20 +67,20 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString())
|
||||
}
|
||||
ksp {
|
||||
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas")
|
||||
}
|
||||
|
||||
lint {
|
||||
baseline = file("lint-baseline.xml")
|
||||
abortOnError true
|
||||
warningsAsErrors true
|
||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||
warning 'ImpliedQuantity'
|
||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||
disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||
disable 'ObsoleteLintCustomCheck'
|
||||
// We manage dependencies on Gitlab with RenovateBot
|
||||
disable 'GradleDependency'
|
||||
textReport true
|
||||
checkDependencies true
|
||||
}
|
||||
@ -85,7 +88,7 @@ android {
|
||||
|
||||
}
|
||||
|
||||
tasks.withType(Test) {
|
||||
tasks.withType(Test).configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@ -129,7 +132,7 @@ dependencies {
|
||||
implementation libs.rxAndroid
|
||||
implementation libs.multiType
|
||||
|
||||
kapt libs.room
|
||||
ksp libs.room
|
||||
|
||||
testImplementation libs.kotlinReflect
|
||||
testImplementation libs.junit
|
||||
@ -141,6 +144,5 @@ dependencies {
|
||||
testImplementation libs.robolectric
|
||||
|
||||
implementation libs.timber
|
||||
|
||||
}
|
||||
|
||||
|
@ -488,8 +488,7 @@ class NavigationActivity : AppCompatActivity() {
|
||||
val downloadHandler: DownloadHandler by inject()
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = musicDirectory.getTracks(),
|
||||
append = false,
|
||||
playNext = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
shuffle = false,
|
||||
fragment = currentFragment,
|
||||
|
@ -98,7 +98,7 @@ class HeaderViewBinder(
|
||||
holder.yearView.text = year
|
||||
|
||||
val songs = resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs, item.childCount,
|
||||
R.plurals.n_songs, item.childCount,
|
||||
item.childCount
|
||||
)
|
||||
holder.songCountView.text = songs
|
||||
|
@ -56,7 +56,7 @@ class BookmarksFragment : TrackCollectionFragment() {
|
||||
super.setupButtons(view)
|
||||
|
||||
playNowButton!!.setOnClickListener {
|
||||
playNow(getSelectedSongs())
|
||||
playNow(getSelectedTracks())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,6 +20,7 @@ import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.domain.Artist
|
||||
import org.moire.ultrasonic.domain.GenericEntry
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
||||
@ -133,27 +134,24 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
fragment,
|
||||
item.id,
|
||||
append = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
playNext = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_play_next ->
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
fragment,
|
||||
item.id,
|
||||
append = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
|
||||
autoPlay = true,
|
||||
playNext = true,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_play_last ->
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
fragment,
|
||||
item.id,
|
||||
append = true,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
|
||||
autoPlay = false,
|
||||
playNext = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
R.id.menu_pin ->
|
||||
|
@ -253,7 +253,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
||||
)
|
||||
mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1)
|
||||
toast(context, resources.getQuantityString(R.plurals.select_album_n_songs_added, 1, 1))
|
||||
toast(context, resources.getQuantityString(R.plurals.n_songs_added_to_end, 1, 1))
|
||||
}
|
||||
|
||||
private fun onVideoSelected(track: Track) {
|
||||
@ -307,8 +307,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
songs.add(item)
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = songs,
|
||||
append = false,
|
||||
playNext = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
shuffle = false,
|
||||
fragment = this,
|
||||
@ -319,8 +318,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
songs.add(item)
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = songs,
|
||||
append = true,
|
||||
playNext = true,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
fragment = this,
|
||||
@ -331,8 +329,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
songs.add(item)
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = songs,
|
||||
append = true,
|
||||
playNext = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
fragment = this,
|
||||
@ -344,7 +341,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
toast(
|
||||
context,
|
||||
resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_pinned,
|
||||
R.plurals.n_songs_pinned,
|
||||
songs.size,
|
||||
songs.size
|
||||
)
|
||||
@ -356,7 +353,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
toast(
|
||||
context,
|
||||
resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_downloaded,
|
||||
R.plurals.n_songs_to_be_downloaded,
|
||||
songs.size,
|
||||
songs.size
|
||||
)
|
||||
@ -368,7 +365,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
||||
toast(
|
||||
context,
|
||||
resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_unpinned,
|
||||
R.plurals.n_songs_unpinned,
|
||||
songs.size,
|
||||
songs.size
|
||||
)
|
||||
|
@ -159,7 +159,7 @@ open class TrackCollectionFragment(
|
||||
// Change the buttons if the status of any selected track changes
|
||||
rxBusSubscription += RxBus.trackDownloadStateObservable.subscribe {
|
||||
if (it.progress != null) return@subscribe
|
||||
val selectedSongs = getSelectedSongs()
|
||||
val selectedSongs = getSelectedTracks()
|
||||
if (!selectedSongs.any { song -> song.id == it.id }) return@subscribe
|
||||
triggerButtonUpdate(selectedSongs)
|
||||
}
|
||||
@ -211,23 +211,15 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
|
||||
playNowButton?.setOnClickListener {
|
||||
playNow(false)
|
||||
playNow(MediaPlayerManager.InsertionMode.CLEAR, toast = true)
|
||||
}
|
||||
|
||||
playNextButton?.setOnClickListener {
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = getSelectedSongs(),
|
||||
append = true,
|
||||
playNext = true,
|
||||
autoPlay = false,
|
||||
shuffle = false,
|
||||
playlistName = navArgs.playlistName,
|
||||
this@TrackCollectionFragment
|
||||
)
|
||||
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, toast = true)
|
||||
}
|
||||
|
||||
playLastButton!!.setOnClickListener {
|
||||
playNow(true)
|
||||
playNow(MediaPlayerManager.InsertionMode.APPEND, toast = true)
|
||||
}
|
||||
|
||||
pinButton?.setOnClickListener {
|
||||
@ -291,7 +283,7 @@ open class TrackCollectionFragment(
|
||||
return true
|
||||
} else if (item.itemId == R.id.menu_item_share) {
|
||||
shareHandler.createShare(
|
||||
this@TrackCollectionFragment, getSelectedSongs(),
|
||||
this@TrackCollectionFragment, getSelectedTracks(),
|
||||
refreshListView, cancellationToken!!,
|
||||
navArgs.id
|
||||
)
|
||||
@ -308,20 +300,37 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
|
||||
private fun playNow(
|
||||
append: Boolean,
|
||||
selectedSongs: List<Track> = getSelectedSongs()
|
||||
insertionMode: MediaPlayerManager.InsertionMode,
|
||||
selectedTracks: List<Track> = getSelectedTracks(),
|
||||
toast: Boolean = false
|
||||
) {
|
||||
if (selectedSongs.isNotEmpty()) {
|
||||
if (selectedTracks.isNotEmpty()) {
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = selectedSongs,
|
||||
append = append,
|
||||
playNext = false,
|
||||
autoPlay = !append,
|
||||
songs = selectedTracks,
|
||||
insertionMode = insertionMode,
|
||||
autoPlay = (insertionMode == MediaPlayerManager.InsertionMode.CLEAR),
|
||||
playlistName = null,
|
||||
fragment = this
|
||||
)
|
||||
} else {
|
||||
playAll(false, append)
|
||||
playAll(false, insertionMode)
|
||||
}
|
||||
|
||||
if (toast) {
|
||||
val stringInt = when (insertionMode) {
|
||||
MediaPlayerManager.InsertionMode.CLEAR ->
|
||||
R.plurals.n_songs_added_play_now
|
||||
MediaPlayerManager.InsertionMode.AFTER_CURRENT ->
|
||||
R.plurals.n_songs_added_after_current
|
||||
MediaPlayerManager.InsertionMode.APPEND ->
|
||||
R.plurals.n_songs_added_to_end
|
||||
}
|
||||
val msg = resources.getQuantityString(
|
||||
stringInt,
|
||||
selectedTracks.size,
|
||||
selectedTracks.size
|
||||
)
|
||||
Util.toast(requireContext(), msg)
|
||||
}
|
||||
}
|
||||
|
||||
@ -338,7 +347,10 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
}
|
||||
|
||||
private fun playAll(shuffle: Boolean = false, append: Boolean = false) {
|
||||
private fun playAll(
|
||||
shuffle: Boolean = false,
|
||||
insertionMode: MediaPlayerManager.InsertionMode = MediaPlayerManager.InsertionMode.CLEAR
|
||||
) {
|
||||
var hasSubFolders = false
|
||||
|
||||
for (item in viewAdapter.getCurrentList()) {
|
||||
@ -355,18 +367,16 @@ open class TrackCollectionFragment(
|
||||
downloadHandler.fetchTracksAndAddToController(
|
||||
fragment = this,
|
||||
id = navArgs.id!!,
|
||||
append = append,
|
||||
autoPlay = !append,
|
||||
insertionMode = insertionMode,
|
||||
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||
shuffle = shuffle,
|
||||
playNext = false,
|
||||
isArtist = isArtist
|
||||
)
|
||||
} else {
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = getAllSongs(),
|
||||
append = append,
|
||||
playNext = false,
|
||||
autoPlay = !append,
|
||||
insertionMode = insertionMode,
|
||||
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||
shuffle = shuffle,
|
||||
playlistName = navArgs.playlistName,
|
||||
fragment = this
|
||||
@ -397,7 +407,7 @@ open class TrackCollectionFragment(
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun triggerButtonUpdate(selection: List<Track> = getSelectedSongs()) {
|
||||
fun triggerButtonUpdate(selection: List<Track> = getSelectedTracks()) {
|
||||
listModel.calculateButtonState(selection, ::updateButtonState)
|
||||
}
|
||||
|
||||
@ -414,14 +424,14 @@ open class TrackCollectionFragment(
|
||||
playNowButton?.isVisible = show.all
|
||||
playNextButton?.isVisible = show.all && multipleSelection
|
||||
playLastButton?.isVisible = show.all && multipleSelection
|
||||
pinButton?.isVisible = show.all && !isOffline() && show.pin
|
||||
pinButton?.isVisible = show.all && show.pin
|
||||
unpinButton?.isVisible = show.all && show.unpin
|
||||
downloadButton?.isVisible = show.all && show.download && !isOffline()
|
||||
deleteButton?.isVisible = show.all && show.delete
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadBackground(save: Boolean, tracks: List<Track> = getSelectedSongs()) {
|
||||
private fun downloadBackground(save: Boolean, tracks: List<Track> = getSelectedTracks()) {
|
||||
var songs = tracks
|
||||
|
||||
if (songs.isEmpty()) {
|
||||
@ -436,7 +446,7 @@ open class TrackCollectionFragment(
|
||||
)
|
||||
}
|
||||
|
||||
internal fun delete(songs: List<Track> = getSelectedSongs()) {
|
||||
internal fun delete(songs: List<Track> = getSelectedTracks()) {
|
||||
downloadHandler.justDownload(
|
||||
action = DownloadAction.DELETE,
|
||||
fragment = this,
|
||||
@ -444,7 +454,7 @@ open class TrackCollectionFragment(
|
||||
)
|
||||
}
|
||||
|
||||
internal fun unpin(songs: List<Track> = getSelectedSongs()) {
|
||||
internal fun unpin(songs: List<Track> = getSelectedTracks()) {
|
||||
downloadHandler.justDownload(
|
||||
action = DownloadAction.UNPIN,
|
||||
fragment = this,
|
||||
@ -502,10 +512,7 @@ open class TrackCollectionFragment(
|
||||
val playAll = navArgs.autoPlay
|
||||
|
||||
if (playAll && songCount > 0) {
|
||||
playAll(
|
||||
navArgs.shuffle,
|
||||
false
|
||||
)
|
||||
playAll(navArgs.shuffle, MediaPlayerManager.InsertionMode.CLEAR)
|
||||
}
|
||||
|
||||
listModel.currentListIsSortable = true
|
||||
@ -513,7 +520,7 @@ open class TrackCollectionFragment(
|
||||
Timber.i("Processed list")
|
||||
}
|
||||
|
||||
internal fun getSelectedSongs(): List<Track> {
|
||||
internal fun getSelectedTracks(): List<Track> {
|
||||
// Walk through selected set and get the Entries based on the saved ids.
|
||||
return viewAdapter.getCurrentList().mapNotNull {
|
||||
if (it is Track && viewAdapter.isSelected(it.longId))
|
||||
@ -608,20 +615,13 @@ open class TrackCollectionFragment(
|
||||
|
||||
when (menuItem.itemId) {
|
||||
R.id.song_menu_play_now -> {
|
||||
playNow(false, songs)
|
||||
playNow(MediaPlayerManager.InsertionMode.CLEAR, songs, true)
|
||||
}
|
||||
R.id.song_menu_play_next -> {
|
||||
downloadHandler.addTracksToMediaController(
|
||||
songs = songs,
|
||||
append = true,
|
||||
playNext = true,
|
||||
autoPlay = false,
|
||||
playlistName = navArgs.playlistName,
|
||||
fragment = this@TrackCollectionFragment
|
||||
)
|
||||
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, songs, true)
|
||||
}
|
||||
R.id.song_menu_play_last -> {
|
||||
playNow(true, songs)
|
||||
playNow(MediaPlayerManager.InsertionMode.APPEND, songs, true)
|
||||
}
|
||||
R.id.song_menu_pin -> {
|
||||
downloadBackground(true, songs)
|
||||
|
@ -35,6 +35,7 @@ import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||
import org.moire.ultrasonic.domain.Share
|
||||
import org.moire.ultrasonic.fragment.FragmentTitle
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.OfflineException
|
||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
||||
@ -165,10 +166,9 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
append = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
shuffle = false,
|
||||
playNext = false,
|
||||
shuffle = false
|
||||
)
|
||||
}
|
||||
R.id.share_menu_play_shuffled -> {
|
||||
@ -176,10 +176,9 @@ class SharesFragment : Fragment(), KoinComponent {
|
||||
this,
|
||||
share.id,
|
||||
share.name,
|
||||
append = false,
|
||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||
autoPlay = true,
|
||||
shuffle = true,
|
||||
playNext = false,
|
||||
)
|
||||
}
|
||||
R.id.share_menu_delete -> {
|
||||
|
@ -11,7 +11,7 @@ import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import androidx.media3.session.BitmapLoader
|
||||
import androidx.media3.common.util.BitmapLoader
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.ListeningExecutorService
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
|
@ -7,16 +7,16 @@
|
||||
|
||||
package org.moire.ultrasonic.playback
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MIXED
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_PLAYLIST
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Rating
|
||||
import androidx.media3.common.StarRating
|
||||
import androidx.media3.session.CommandButton
|
||||
@ -45,7 +45,6 @@ import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.SearchCriteria
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.RatingManager
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -96,11 +95,8 @@ const val PLAY_COMMAND = "play "
|
||||
* MediaBrowserService implementation for e.g. Android Auto
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
||||
@SuppressLint("UnsafeOptInUsageError")
|
||||
class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
||||
class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
||||
|
||||
private val mediaPlayerManager by inject<MediaPlayerManager>()
|
||||
private val activeServerProvider: ActiveServerProvider by inject()
|
||||
|
||||
private val serviceJob = SupervisorJob()
|
||||
@ -116,22 +112,59 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
private val isOffline get() = ActiveServerProvider.isOffline()
|
||||
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
||||
|
||||
private var customCommands: List<CommandButton>
|
||||
internal var customLayout = ImmutableList.of<CommandButton>()
|
||||
private val placeholderButton = getPlaceholderButton()
|
||||
|
||||
private var heartIsCurrentlyOn = false
|
||||
|
||||
// This button is used for an unstarred track, and its action will star the track
|
||||
private val heartButtonToggleOn =
|
||||
getHeartCommandButton(
|
||||
SessionCommand(
|
||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON,
|
||||
Bundle.EMPTY
|
||||
),
|
||||
willHeart = true
|
||||
)
|
||||
|
||||
// This button is used for an starred track, and its action will star the track
|
||||
private val heartButtonToggleOff =
|
||||
getHeartCommandButton(
|
||||
SessionCommand(
|
||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF,
|
||||
Bundle.EMPTY
|
||||
),
|
||||
willHeart = false
|
||||
)
|
||||
|
||||
private val shuffleButton: CommandButton
|
||||
|
||||
private val repeatOffButton: CommandButton
|
||||
private val repeatOneButton: CommandButton
|
||||
private val repeatAllButton: CommandButton
|
||||
|
||||
private val allCustomCommands: List<CommandButton>
|
||||
|
||||
val defaultCustomCommands: List<CommandButton>
|
||||
|
||||
init {
|
||||
customCommands =
|
||||
listOf(
|
||||
// This button is used for an unstarred track, and its action will star the track
|
||||
getHeartCommandButton(
|
||||
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY)
|
||||
),
|
||||
// This button is used for an starred track, and its action will unstar the track
|
||||
getHeartCommandButton(
|
||||
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY)
|
||||
)
|
||||
)
|
||||
customLayout = ImmutableList.of(customCommands[0])
|
||||
val shuffleCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_SHUFFLE, Bundle.EMPTY)
|
||||
shuffleButton = getShuffleCommandButton(shuffleCommand)
|
||||
|
||||
val repeatCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_REPEAT_MODE, Bundle.EMPTY)
|
||||
repeatOffButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_OFF)
|
||||
repeatOneButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ONE)
|
||||
repeatAllButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ALL)
|
||||
|
||||
allCustomCommands = listOf(
|
||||
heartButtonToggleOn,
|
||||
heartButtonToggleOff,
|
||||
shuffleButton,
|
||||
repeatOffButton,
|
||||
repeatOneButton,
|
||||
repeatAllButton
|
||||
)
|
||||
|
||||
defaultCustomCommands = listOf(heartButtonToggleOn, shuffleButton, repeatOffButton)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -175,8 +208,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
"Root Folder",
|
||||
MEDIA_ROOT_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
isBrowsable = true,
|
||||
mediaType = MEDIA_TYPE_FOLDER_MIXED
|
||||
),
|
||||
params
|
||||
)
|
||||
@ -188,14 +221,17 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
Timber.i("onConnect")
|
||||
|
||||
val connectionResult = super.onConnect(session, controller)
|
||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||
|
||||
for (commandButton in customCommands) {
|
||||
for (commandButton in allCustomCommands) {
|
||||
// Add custom command to available session commands.
|
||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||
}
|
||||
|
||||
session.player.repeatMode = Player.REPEAT_MODE_ALL
|
||||
|
||||
return MediaSession.ConnectionResult.accept(
|
||||
availableSessionCommands.build(),
|
||||
connectionResult.availablePlayerCommands
|
||||
@ -203,26 +239,72 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
}
|
||||
|
||||
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
|
||||
if (controller.controllerVersion != 0) {
|
||||
// Let Media3 controller (for instance the MediaNotificationProvider)
|
||||
// know about the custom layout right after it connected.
|
||||
session.setCustomLayout(customLayout)
|
||||
with(session) {
|
||||
setCustomLayout(session.buildCustomCommands(canShuffle = canShuffle()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHeartCommandButton(sessionCommand: SessionCommand): CommandButton {
|
||||
val willHeart =
|
||||
(sessionCommand.customAction == PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON)
|
||||
return CommandButton.Builder()
|
||||
.setDisplayName("Love")
|
||||
private fun getHeartCommandButton(sessionCommand: SessionCommand, willHeart: Boolean) =
|
||||
CommandButton.Builder()
|
||||
.setDisplayName(
|
||||
if (willHeart)
|
||||
"Love"
|
||||
else
|
||||
"Dislike"
|
||||
)
|
||||
.setIconResId(
|
||||
if (willHeart) R.drawable.ic_star_hollow
|
||||
else R.drawable.ic_star_full
|
||||
if (willHeart)
|
||||
R.drawable.ic_star_hollow
|
||||
else
|
||||
R.drawable.ic_star_full
|
||||
)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setEnabled(true)
|
||||
.build()
|
||||
|
||||
private fun getShuffleCommandButton(sessionCommand: SessionCommand) =
|
||||
CommandButton.Builder()
|
||||
.setDisplayName("Shuffle")
|
||||
.setIconResId(R.drawable.media_shuffle)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setEnabled(true)
|
||||
.build()
|
||||
|
||||
private fun getPlaceholderButton() = CommandButton.Builder()
|
||||
.setDisplayName("Placeholder")
|
||||
.setIconResId(android.R.color.transparent)
|
||||
.setSessionCommand(
|
||||
SessionCommand(
|
||||
PlaybackService.CUSTOM_COMMAND_PLACEHOLDER,
|
||||
Bundle.EMPTY
|
||||
)
|
||||
)
|
||||
.setEnabled(false)
|
||||
.build()
|
||||
|
||||
private fun getRepeatModeButton(sessionCommand: SessionCommand, repeatMode: Int) =
|
||||
CommandButton.Builder()
|
||||
.setDisplayName(
|
||||
when (repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> "Repeat One"
|
||||
Player.REPEAT_MODE_ALL -> "Repeat All"
|
||||
else -> "Repeat None"
|
||||
}
|
||||
)
|
||||
.setIconResId(
|
||||
when (repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> R.drawable.media_repeat_one
|
||||
Player.REPEAT_MODE_ALL -> R.drawable.media_repeat_all
|
||||
else -> R.drawable.media_repeat_off
|
||||
}
|
||||
)
|
||||
.setSessionCommand(sessionCommand)
|
||||
.setEnabled(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onGetItem(
|
||||
session: MediaLibraryService.MediaLibrarySession,
|
||||
@ -266,18 +348,32 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
Timber.i("onCustomCommand")
|
||||
Timber.i("onCustomCommand %s", customCommand.customAction)
|
||||
var customCommandFuture: ListenableFuture<SessionResult>? = null
|
||||
|
||||
when (customCommand.customAction) {
|
||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> {
|
||||
customCommandFuture = onSetRating(session, controller, HeartRating(true))
|
||||
updateCustomHeartButton(session, true)
|
||||
updateCustomHeartButton(session, isHeart = true)
|
||||
}
|
||||
|
||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> {
|
||||
customCommandFuture = onSetRating(session, controller, HeartRating(false))
|
||||
updateCustomHeartButton(session, false)
|
||||
updateCustomHeartButton(session, isHeart = false)
|
||||
}
|
||||
|
||||
PlaybackService.CUSTOM_COMMAND_SHUFFLE -> {
|
||||
customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
|
||||
shuffleCurrentPlaylist(session.player)
|
||||
}
|
||||
|
||||
PlaybackService.CUSTOM_COMMAND_REPEAT_MODE -> {
|
||||
customCommandFuture = Futures.immediateFuture(SessionResult(RESULT_SUCCESS))
|
||||
|
||||
session.player.setNextRepeatMode()
|
||||
session.updateCustomCommands()
|
||||
}
|
||||
|
||||
else -> {
|
||||
Timber.d(
|
||||
"CustomCommand not recognized %s with extra %s",
|
||||
@ -286,16 +382,23 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
)
|
||||
}
|
||||
}
|
||||
if (customCommandFuture != null)
|
||||
return customCommandFuture
|
||||
return super.onCustomCommand(session, controller, customCommand, args)
|
||||
|
||||
return customCommandFuture
|
||||
?: super.onCustomCommand(
|
||||
session,
|
||||
controller,
|
||||
customCommand,
|
||||
args
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSetRating(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
rating: Rating
|
||||
): ListenableFuture<SessionResult> {
|
||||
val mediaItem = session.player.currentMediaItem
|
||||
|
||||
if (mediaItem != null) {
|
||||
if (rating is HeartRating) {
|
||||
mediaItem.toTrack().starred = rating.isHeart
|
||||
@ -309,6 +412,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
rating
|
||||
)
|
||||
}
|
||||
|
||||
return super.onSetRating(session, controller, rating)
|
||||
}
|
||||
|
||||
@ -378,6 +482,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
private fun onAddLegacyAutoItems(
|
||||
mediaItems: MutableList<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
Timber.i("onAddLegacyAutoItems %s", mediaItems.first().mediaId)
|
||||
|
||||
val mediaIdParts = mediaItems.first().mediaId.split('|')
|
||||
|
||||
val tracks = when (mediaIdParts.first()) {
|
||||
@ -385,10 +491,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||
)
|
||||
|
||||
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||
)
|
||||
|
||||
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
||||
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
|
||||
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
|
||||
@ -400,57 +508,59 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
||||
mediaIdParts[1], mediaIdParts[2]
|
||||
)
|
||||
|
||||
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (tracks != null) {
|
||||
return Futures.immediateFuture(
|
||||
tracks.map { track -> track.toMediaItem() }
|
||||
.toMutableList()
|
||||
)
|
||||
}
|
||||
|
||||
// Fallback to the original list
|
||||
return Futures.immediateFuture(mediaItems)
|
||||
return tracks
|
||||
?.let {
|
||||
Futures.immediateFuture(
|
||||
it.map { track -> track.toMediaItem() }
|
||||
.toMutableList()
|
||||
)
|
||||
}
|
||||
?: Futures.immediateFuture(mediaItems)
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount", "ComplexMethod")
|
||||
fun onLoadChildren(
|
||||
private fun onLoadChildren(
|
||||
parentId: String,
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
||||
|
||||
val parentIdParts = parentId.split('|')
|
||||
|
||||
when (parentIdParts.first()) {
|
||||
MEDIA_ROOT_ID -> return getRootItems()
|
||||
MEDIA_LIBRARY_ID -> return getLibrary()
|
||||
MEDIA_ARTIST_ID -> return getArtists()
|
||||
MEDIA_ARTIST_SECTION -> return getArtists(parentIdParts[1])
|
||||
MEDIA_ALBUM_ID -> return getAlbums(AlbumListType.SORTED_BY_NAME)
|
||||
MEDIA_ALBUM_PAGE_ID -> return getAlbums(
|
||||
return when (parentIdParts.first()) {
|
||||
MEDIA_ROOT_ID -> getRootItems()
|
||||
MEDIA_LIBRARY_ID -> getLibrary()
|
||||
MEDIA_ARTIST_ID -> getArtists()
|
||||
MEDIA_ARTIST_SECTION -> getArtists(parentIdParts[1])
|
||||
MEDIA_ALBUM_ID -> getAlbums(AlbumListType.SORTED_BY_NAME)
|
||||
MEDIA_ALBUM_PAGE_ID -> getAlbums(
|
||||
AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
|
||||
)
|
||||
MEDIA_PLAYLIST_ID -> return getPlaylists()
|
||||
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT)
|
||||
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST)
|
||||
MEDIA_ALBUM_RECENT_ID -> return getAlbums(AlbumListType.RECENT)
|
||||
MEDIA_ALBUM_RANDOM_ID -> return getAlbums(AlbumListType.RANDOM)
|
||||
MEDIA_ALBUM_STARRED_ID -> return getAlbums(AlbumListType.STARRED)
|
||||
MEDIA_SONG_RANDOM_ID -> return getRandomSongs()
|
||||
MEDIA_SONG_STARRED_ID -> return getStarredSongs()
|
||||
MEDIA_SHARE_ID -> return getShares()
|
||||
MEDIA_BOOKMARK_ID -> return getBookmarks()
|
||||
MEDIA_PODCAST_ID -> return getPodcasts()
|
||||
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2])
|
||||
MEDIA_ARTIST_ITEM -> return getAlbumsForArtist(
|
||||
|
||||
MEDIA_PLAYLIST_ID -> getPlaylists()
|
||||
MEDIA_ALBUM_FREQUENT_ID -> getAlbums(AlbumListType.FREQUENT)
|
||||
MEDIA_ALBUM_NEWEST_ID -> getAlbums(AlbumListType.NEWEST)
|
||||
MEDIA_ALBUM_RECENT_ID -> getAlbums(AlbumListType.RECENT)
|
||||
MEDIA_ALBUM_RANDOM_ID -> getAlbums(AlbumListType.RANDOM)
|
||||
MEDIA_ALBUM_STARRED_ID -> getAlbums(AlbumListType.STARRED)
|
||||
MEDIA_SONG_RANDOM_ID -> getRandomSongs()
|
||||
MEDIA_SONG_STARRED_ID -> getStarredSongs()
|
||||
MEDIA_SHARE_ID -> getShares()
|
||||
MEDIA_BOOKMARK_ID -> getBookmarks()
|
||||
MEDIA_PODCAST_ID -> getPodcasts()
|
||||
MEDIA_PLAYLIST_ITEM -> getPlaylist(parentIdParts[1], parentIdParts[2])
|
||||
MEDIA_ARTIST_ITEM -> getAlbumsForArtist(
|
||||
parentIdParts[1], parentIdParts[2]
|
||||
)
|
||||
MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
||||
MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1])
|
||||
MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1])
|
||||
else -> return Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
|
||||
|
||||
MEDIA_ALBUM_ITEM -> getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
||||
MEDIA_SHARE_ITEM -> getSongsForShare(parentIdParts[1])
|
||||
MEDIA_PODCAST_ITEM -> getPodcastEpisodes(parentIdParts[1])
|
||||
else -> Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,8 +593,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
mediaItems.add(
|
||||
album.title ?: "",
|
||||
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
||||
.joinToString("|"),
|
||||
FOLDER_TYPE_ALBUMS
|
||||
.joinToString("|")
|
||||
)
|
||||
}
|
||||
|
||||
@ -534,10 +643,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||
)
|
||||
|
||||
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||
)
|
||||
|
||||
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
||||
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
|
||||
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
|
||||
@ -549,6 +660,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
||||
mediaIdParts[1], mediaIdParts[2]
|
||||
)
|
||||
|
||||
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
||||
else -> {
|
||||
listOf()
|
||||
@ -573,7 +685,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
R.string.music_library_label,
|
||||
MEDIA_LIBRARY_ID,
|
||||
null,
|
||||
folderType = FOLDER_TYPE_MIXED,
|
||||
isBrowsable = true,
|
||||
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
||||
icon = R.drawable.ic_library
|
||||
)
|
||||
|
||||
@ -581,7 +694,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
R.string.main_artists_title,
|
||||
MEDIA_ARTIST_ID,
|
||||
null,
|
||||
folderType = FOLDER_TYPE_ARTISTS,
|
||||
isBrowsable = true,
|
||||
mediaType = MEDIA_TYPE_FOLDER_ARTISTS,
|
||||
icon = R.drawable.ic_artist
|
||||
)
|
||||
|
||||
@ -590,7 +704,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
R.string.main_albums_title,
|
||||
MEDIA_ALBUM_ID,
|
||||
null,
|
||||
folderType = FOLDER_TYPE_ALBUMS,
|
||||
isBrowsable = true,
|
||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||
icon = R.drawable.ic_menu_browse
|
||||
)
|
||||
|
||||
@ -598,7 +713,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
R.string.playlist_label,
|
||||
MEDIA_PLAYLIST_ID,
|
||||
null,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS,
|
||||
isBrowsable = true,
|
||||
mediaType = MEDIA_TYPE_FOLDER_PLAYLISTS,
|
||||
icon = R.drawable.ic_menu_playlists
|
||||
)
|
||||
|
||||
@ -613,14 +729,16 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
R.string.main_songs_random,
|
||||
MEDIA_SONG_RANDOM_ID,
|
||||
R.string.main_songs_title,
|
||||
folderType = FOLDER_TYPE_TITLES
|
||||
isBrowsable = true,
|
||||
mediaType = MEDIA_TYPE_PLAYLIST
|
||||
)
|
||||
|
||||
mediaItems.add(
|
||||
R.string.main_songs_starred,
|
||||
MEDIA_SONG_STARRED_ID,
|
||||
R.string.main_songs_title,
|
||||
folderType = FOLDER_TYPE_TITLES
|
||||
isBrowsable = true,
|
||||
mediaType = MEDIA_TYPE_PLAYLIST
|
||||
)
|
||||
|
||||
// Albums
|
||||
@ -634,28 +752,28 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
R.string.main_albums_recent,
|
||||
MEDIA_ALBUM_RECENT_ID,
|
||||
R.string.main_albums_title,
|
||||
folderType = FOLDER_TYPE_ALBUMS
|
||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||
)
|
||||
|
||||
mediaItems.add(
|
||||
R.string.main_albums_frequent,
|
||||
MEDIA_ALBUM_FREQUENT_ID,
|
||||
R.string.main_albums_title,
|
||||
folderType = FOLDER_TYPE_ALBUMS
|
||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||
)
|
||||
|
||||
mediaItems.add(
|
||||
R.string.main_albums_random,
|
||||
MEDIA_ALBUM_RANDOM_ID,
|
||||
R.string.main_albums_title,
|
||||
folderType = FOLDER_TYPE_ALBUMS
|
||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||
)
|
||||
|
||||
mediaItems.add(
|
||||
R.string.main_albums_starred,
|
||||
MEDIA_ALBUM_STARRED_ID,
|
||||
R.string.main_albums_title,
|
||||
folderType = FOLDER_TYPE_ALBUMS
|
||||
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||
)
|
||||
|
||||
// Other
|
||||
@ -704,8 +822,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
index.add(currentSection)
|
||||
mediaItems.add(
|
||||
currentSection,
|
||||
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
|
||||
FOLDER_TYPE_ARTISTS
|
||||
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|")
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -713,8 +830,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
artists.map { artist ->
|
||||
mediaItems.add(
|
||||
artist.name ?: "",
|
||||
listOf(childMediaId, artist.id, artist.name).joinToString("|"),
|
||||
FOLDER_TYPE_ARTISTS
|
||||
listOf(childMediaId, artist.id, artist.name).joinToString("|")
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -744,8 +860,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
mediaItems.add(
|
||||
album.title ?: "",
|
||||
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
||||
.joinToString("|"),
|
||||
FOLDER_TYPE_ALBUMS
|
||||
.joinToString("|")
|
||||
)
|
||||
}
|
||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||
@ -768,15 +883,24 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
||||
|
||||
// TODO: Paging is not implemented for songs, is it necessary at all?
|
||||
val items = songs.getTracks().take(DISPLAY_LIMIT)
|
||||
val items = songs.getChildren().take(DISPLAY_LIMIT).toMutableList()
|
||||
|
||||
items.sortWith { o1, o2 ->
|
||||
if (o1.isDirectory && o2.isDirectory)
|
||||
(o1.title ?: "").compareTo(o2.title ?: "")
|
||||
else if (o1.isDirectory)
|
||||
-1
|
||||
else
|
||||
1
|
||||
}
|
||||
|
||||
items.map { item ->
|
||||
if (item.isDirectory)
|
||||
mediaItems.add(
|
||||
item.title ?: "",
|
||||
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|"),
|
||||
FOLDER_TYPE_TITLES
|
||||
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|")
|
||||
)
|
||||
else
|
||||
else if (item is Track)
|
||||
mediaItems.add(
|
||||
item.toMediaItem(
|
||||
listOf(
|
||||
@ -789,6 +913,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||
}
|
||||
}
|
||||
@ -822,8 +947,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
mediaItems.add(
|
||||
album.title ?: "",
|
||||
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
||||
.joinToString("|"),
|
||||
FOLDER_TYPE_ALBUMS
|
||||
.joinToString("|")
|
||||
)
|
||||
}
|
||||
|
||||
@ -851,7 +975,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
playlist.name,
|
||||
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
||||
.joinToString("|"),
|
||||
FOLDER_TYPE_PLAYLISTS
|
||||
mediaType = MEDIA_TYPE_PLAYLIST,
|
||||
)
|
||||
}
|
||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||
@ -945,7 +1069,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
mediaItems.add(
|
||||
podcast.title ?: "",
|
||||
listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"),
|
||||
FOLDER_TYPE_MIXED
|
||||
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
||||
)
|
||||
}
|
||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||
@ -1048,7 +1172,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
share.name ?: "",
|
||||
listOf(MEDIA_SHARE_ITEM, share.id)
|
||||
.joinToString("|"),
|
||||
FOLDER_TYPE_MIXED
|
||||
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
||||
)
|
||||
}
|
||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||
@ -1226,14 +1350,16 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
private fun MutableList<MediaItem>.add(
|
||||
title: String,
|
||||
mediaId: String,
|
||||
folderType: Int
|
||||
mediaType: Int = MEDIA_TYPE_MIXED,
|
||||
isBrowsable: Boolean = false
|
||||
) {
|
||||
|
||||
val mediaItem = buildMediaItem(
|
||||
title,
|
||||
mediaId,
|
||||
isPlayable = false,
|
||||
folderType = folderType
|
||||
isBrowsable = isBrowsable,
|
||||
mediaType = mediaType
|
||||
)
|
||||
|
||||
this.add(mediaItem)
|
||||
@ -1244,8 +1370,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
resId: Int,
|
||||
mediaId: String,
|
||||
groupNameId: Int?,
|
||||
browsable: Boolean = true,
|
||||
folderType: Int = FOLDER_TYPE_MIXED,
|
||||
isBrowsable: Boolean = true,
|
||||
mediaType: Int = MEDIA_TYPE_FOLDER_MIXED,
|
||||
icon: Int? = null
|
||||
) {
|
||||
val applicationContext = UApp.applicationContext()
|
||||
@ -1253,14 +1379,15 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
val mediaItem = buildMediaItem(
|
||||
applicationContext.getString(resId),
|
||||
mediaId,
|
||||
isPlayable = !browsable,
|
||||
folderType = folderType,
|
||||
isPlayable = !isBrowsable,
|
||||
isBrowsable = isBrowsable,
|
||||
imageUri = if (icon != null) {
|
||||
Util.getUriToDrawable(applicationContext, icon)
|
||||
} else null,
|
||||
group = if (groupNameId != null) {
|
||||
applicationContext.getString(groupNameId)
|
||||
} else null,
|
||||
imageUri = if (icon != null) {
|
||||
Util.getUriToDrawable(applicationContext, icon)
|
||||
} else null
|
||||
mediaType = mediaType
|
||||
)
|
||||
|
||||
this.add(mediaItem)
|
||||
@ -1294,14 +1421,102 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCustomHeartButton(
|
||||
session: MediaSession,
|
||||
isHeart: Boolean
|
||||
) {
|
||||
val command = if (isHeart) customCommands[1] else customCommands[0]
|
||||
// Change the custom layout to contain the right heart button
|
||||
customLayout = ImmutableList.of(command)
|
||||
// Send the updated custom layout to controllers.
|
||||
session.setCustomLayout(customLayout)
|
||||
private fun Player.setNextRepeatMode() {
|
||||
repeatMode =
|
||||
when (repeatMode) {
|
||||
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL
|
||||
Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE
|
||||
else -> Player.REPEAT_MODE_OFF
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaSession.updateCustomCommands() {
|
||||
setCustomLayout(
|
||||
buildCustomCommands(
|
||||
heartIsCurrentlyOn,
|
||||
canShuffle()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun updateCustomHeartButton(session: MediaSession, isHeart: Boolean) {
|
||||
with(session) {
|
||||
setCustomLayout(
|
||||
buildCustomCommands(
|
||||
isHeart = isHeart,
|
||||
canShuffle = canShuffle()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MediaSession.canShuffle() =
|
||||
player.mediaItemCount > 2
|
||||
|
||||
private fun MediaSession.buildCustomCommands(
|
||||
isHeart: Boolean = false,
|
||||
canShuffle: Boolean = false
|
||||
): ImmutableList<CommandButton> {
|
||||
Timber.d("building custom commands (isHeart = %s, canShuffle = %s)", isHeart, canShuffle)
|
||||
|
||||
heartIsCurrentlyOn = isHeart
|
||||
|
||||
return ImmutableList.copyOf(
|
||||
buildList {
|
||||
// placeholder must come first here because if there is no next button the first
|
||||
// custom command button is place right next to the play/pause button
|
||||
if (
|
||||
player.repeatMode != Player.REPEAT_MODE_ALL &&
|
||||
player.currentMediaItemIndex == player.mediaItemCount - 1
|
||||
)
|
||||
add(placeholderButton)
|
||||
|
||||
// due to the previous placeholder this heart button will always appear to the left
|
||||
// of the default playback items
|
||||
add(
|
||||
if (isHeart)
|
||||
heartButtonToggleOff
|
||||
else
|
||||
heartButtonToggleOn
|
||||
)
|
||||
|
||||
// both the shuffle and the active repeat mode button will end up in the overflow
|
||||
// menu if both are available at the same time
|
||||
if (canShuffle)
|
||||
add(shuffleButton)
|
||||
|
||||
add(
|
||||
when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> repeatOneButton
|
||||
Player.REPEAT_MODE_ALL -> repeatAllButton
|
||||
else -> repeatOffButton
|
||||
}
|
||||
)
|
||||
}.asIterable()
|
||||
)
|
||||
}
|
||||
|
||||
private fun shuffleCurrentPlaylist(player: Player) {
|
||||
Timber.d("shuffleCurrentPlaylist")
|
||||
|
||||
// 3 was chosen because that leaves at least two other songs to be shuffled around
|
||||
@Suppress("MagicNumber")
|
||||
if (player.mediaItemCount < 3)
|
||||
return
|
||||
|
||||
val mediaItemsToShuffle = mutableListOf<MediaItem>()
|
||||
|
||||
for (i in 0 until player.currentMediaItemIndex) {
|
||||
mediaItemsToShuffle += player.getMediaItemAt(i)
|
||||
}
|
||||
|
||||
for (i in player.currentMediaItemIndex + 1 until player.mediaItemCount) {
|
||||
mediaItemsToShuffle += player.getMediaItemAt(i)
|
||||
}
|
||||
|
||||
player.removeMediaItems(player.currentMediaItemIndex + 1, player.mediaItemCount)
|
||||
player.removeMediaItems(0, player.currentMediaItemIndex)
|
||||
|
||||
player.addMediaItems(mediaItemsToShuffle.shuffled())
|
||||
}
|
||||
}
|
||||
|
@ -1,40 +0,0 @@
|
||||
/*
|
||||
* CustomNotificationProvider.kt
|
||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
package org.moire.ultrasonic.playback
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.MediaSession
|
||||
import com.google.common.collect.ImmutableList
|
||||
import org.koin.core.component.KoinComponent
|
||||
|
||||
@UnstableApi
|
||||
class CustomNotificationProvider(ctx: Context) :
|
||||
DefaultMediaNotificationProvider(ctx),
|
||||
KoinComponent {
|
||||
|
||||
// By default the skip buttons are not shown in compact view.
|
||||
// We add the COMMAND_KEY_COMPACT_VIEW_INDEX to show them
|
||||
// See also: https://github.com/androidx/media/issues/410
|
||||
override fun getMediaButtons(
|
||||
session: MediaSession,
|
||||
playerCommands: Player.Commands,
|
||||
customLayout: ImmutableList<CommandButton>,
|
||||
playWhenReady: Boolean
|
||||
): ImmutableList<CommandButton> {
|
||||
val commands = super.getMediaButtons(session, playerCommands, customLayout, playWhenReady)
|
||||
|
||||
commands.forEachIndexed { index, command ->
|
||||
command.extras.putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, index)
|
||||
}
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
@ -130,26 +130,19 @@ class PlaybackService :
|
||||
private fun initializeSessionAndPlayer() {
|
||||
if (isStarted) return
|
||||
|
||||
setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext()))
|
||||
|
||||
// TODO: Remove minor code duplication with updateBackend()
|
||||
val desiredBackend = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
||||
Timber.i("Jukebox enabled by default")
|
||||
MediaPlayerManager.PlayerBackend.JUKEBOX
|
||||
} else {
|
||||
MediaPlayerManager.PlayerBackend.LOCAL
|
||||
}
|
||||
|
||||
player = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
||||
Timber.i("Jukebox enabled by default")
|
||||
getJukeboxPlayer()
|
||||
} else {
|
||||
getLocalPlayer()
|
||||
}
|
||||
player = createNewBackend(desiredBackend)
|
||||
|
||||
actualBackend = desiredBackend
|
||||
|
||||
// Create browser interface
|
||||
librarySessionCallback = AutoMediaBrowserCallback(this)
|
||||
librarySessionCallback = AutoMediaBrowserCallback()
|
||||
|
||||
// This will need to use the AutoCalls
|
||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
@ -157,10 +150,8 @@ class PlaybackService :
|
||||
.setBitmapLoader(ArtworkBitmapLoader())
|
||||
.build()
|
||||
|
||||
if (!librarySessionCallback.customLayout.isEmpty()) {
|
||||
// Send custom layout to legacy session.
|
||||
mediaLibrarySession.setCustomLayout(librarySessionCallback.customLayout)
|
||||
}
|
||||
// Send custom layout to legacy session.
|
||||
mediaLibrarySession.setCustomLayout(librarySessionCallback.defaultCustomCommands)
|
||||
|
||||
// Set a listener to update the API client when the active server has changed
|
||||
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||
@ -213,11 +204,7 @@ class PlaybackService :
|
||||
player.removeListener(listener)
|
||||
player.release()
|
||||
|
||||
player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) {
|
||||
getJukeboxPlayer()
|
||||
} else {
|
||||
getLocalPlayer()
|
||||
}
|
||||
player = createNewBackend(newBackend)
|
||||
|
||||
// Add fresh listeners
|
||||
player.addListener(listener)
|
||||
@ -227,6 +214,14 @@ class PlaybackService :
|
||||
actualBackend = newBackend
|
||||
}
|
||||
|
||||
private fun createNewBackend(newBackend: MediaPlayerManager.PlayerBackend): Player {
|
||||
return if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) {
|
||||
getJukeboxPlayer()
|
||||
} else {
|
||||
getLocalPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getJukeboxPlayer(): Player {
|
||||
return JukeboxMediaPlayer()
|
||||
}
|
||||
@ -425,6 +420,12 @@ class PlaybackService :
|
||||
"org.moire.ultrasonic.HEART_ON"
|
||||
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF =
|
||||
"org.moire.ultrasonic.HEART_OFF"
|
||||
const val CUSTOM_COMMAND_SHUFFLE =
|
||||
"org.moire.ultrasonic.SHUFFLE"
|
||||
const val CUSTOM_COMMAND_PLACEHOLDER =
|
||||
"org.moire.ultrasonic.PLACEHOLDER"
|
||||
const val CUSTOM_COMMAND_REPEAT_MODE =
|
||||
"org.moire.ultrasonic.REPEAT_MODE"
|
||||
private const val NOTIFICATION_ID = 3009
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
companion object {
|
||||
// This is quite important, by setting the DeviceInfo the player is recognized by
|
||||
// Android as being a remote playback surface
|
||||
val DEVICE_INFO = DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, 0, 10)
|
||||
val DEVICE_INFO = DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
|
||||
.setMinVolume(0)
|
||||
.setMaxVolume(10)
|
||||
.build()
|
||||
val running = AtomicBoolean()
|
||||
const val MAX_GAIN = 10
|
||||
}
|
||||
@ -206,14 +209,14 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
Player.COMMAND_CHANGE_MEDIA_ITEMS,
|
||||
Player.COMMAND_GET_TIMELINE,
|
||||
Player.COMMAND_GET_DEVICE_VOLUME,
|
||||
Player.COMMAND_ADJUST_DEVICE_VOLUME,
|
||||
Player.COMMAND_SET_DEVICE_VOLUME
|
||||
Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
|
||||
Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
|
||||
)
|
||||
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
||||
if (playlist.isNotEmpty()) {
|
||||
commandsBuilder.addAll(
|
||||
Player.COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||
Player.COMMAND_GET_MEDIA_ITEMS_METADATA,
|
||||
Player.COMMAND_GET_METADATA,
|
||||
Player.COMMAND_PLAY_PAUSE,
|
||||
Player.COMMAND_PREPARE,
|
||||
Player.COMMAND_SEEK_BACK,
|
||||
@ -284,6 +287,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
||||
|
||||
override fun setDeviceVolume(volume: Int) {
|
||||
setDeviceVolume(volume, 0)
|
||||
}
|
||||
|
||||
override fun setDeviceVolume(volume: Int, flags: Int) {
|
||||
gain = volume
|
||||
tasks.remove(SetGain::class.java)
|
||||
tasks.add(SetGain(floatGain))
|
||||
@ -299,17 +306,32 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun increaseDeviceVolume() {
|
||||
increaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI)
|
||||
}
|
||||
|
||||
override fun increaseDeviceVolume(flags: Int) {
|
||||
gain = (gain + 1).coerceAtMost(MAX_GAIN)
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun decreaseDeviceVolume() {
|
||||
decreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI)
|
||||
}
|
||||
|
||||
override fun decreaseDeviceVolume(flags: Int) {
|
||||
gain = (gain - 1).coerceAtLeast(0)
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun setDeviceMuted(muted: Boolean) {
|
||||
setDeviceMuted(muted, C.VOLUME_FLAG_SHOW_UI)
|
||||
}
|
||||
|
||||
override fun setDeviceMuted(muted: Boolean, flags: Int) {
|
||||
gain = 0
|
||||
deviceVolume = gain
|
||||
}
|
||||
|
@ -67,6 +67,18 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun replaceMediaItem(index: Int, mediaItem: MediaItem) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun replaceMediaItems(
|
||||
fromIndex: Int,
|
||||
toIndex: Int,
|
||||
mediaItems: MutableList<MediaItem>
|
||||
) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
@ -134,11 +146,6 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
||||
override fun setPlaybackSpeed(speed: Float) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun stop(reset: Boolean) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun getCurrentTracks(): Tracks {
|
||||
// TODO Dummy information is returned for now, this seems to work
|
||||
return Tracks.EMPTY
|
||||
|
@ -460,7 +460,7 @@ class MediaPlayerManager(
|
||||
// We can't just use play(0,0) then all random playlists will start with the first track.
|
||||
// Additionally the shuffle order becomes clear on after some time, so we need to wait for
|
||||
// the right event, and can start playback only then.
|
||||
if (autoPlay) {
|
||||
if (autoPlay && controller?.isPlaying != true) {
|
||||
if (isShufflePlayEnabled) {
|
||||
deferredPlay = {
|
||||
val start = controller?.currentTimeline
|
||||
|
@ -68,27 +68,27 @@ class DownloadHandler(
|
||||
}
|
||||
successString = when (action) {
|
||||
DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_downloaded,
|
||||
R.plurals.n_songs_to_be_downloaded,
|
||||
tracksToDownload.size,
|
||||
tracksToDownload.size
|
||||
)
|
||||
DownloadAction.UNPIN -> {
|
||||
fragment.resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_unpinned,
|
||||
R.plurals.n_songs_unpinned,
|
||||
tracksToDownload.size,
|
||||
tracksToDownload.size
|
||||
)
|
||||
}
|
||||
DownloadAction.PIN -> {
|
||||
fragment.resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_pinned,
|
||||
R.plurals.n_songs_pinned,
|
||||
tracksToDownload.size,
|
||||
tracksToDownload.size
|
||||
)
|
||||
}
|
||||
DownloadAction.DELETE -> {
|
||||
fragment.resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_deleted,
|
||||
R.plurals.n_songs_deleted,
|
||||
tracksToDownload.size,
|
||||
tracksToDownload.size
|
||||
)
|
||||
@ -104,10 +104,9 @@ class DownloadHandler(
|
||||
name: String? = "",
|
||||
isShare: Boolean = false,
|
||||
isDirectory: Boolean = true,
|
||||
append: Boolean,
|
||||
insertionMode: MediaPlayerManager.InsertionMode,
|
||||
autoPlay: Boolean,
|
||||
shuffle: Boolean = false,
|
||||
playNext: Boolean,
|
||||
isArtist: Boolean = false
|
||||
) {
|
||||
var successString: String? = null
|
||||
@ -119,26 +118,28 @@ class DownloadHandler(
|
||||
withContext(Dispatchers.Main) {
|
||||
addTracksToMediaController(
|
||||
songs = songs,
|
||||
append = append,
|
||||
playNext = playNext,
|
||||
insertionMode = insertionMode,
|
||||
autoPlay = autoPlay,
|
||||
shuffle = shuffle,
|
||||
playlistName = null,
|
||||
fragment = fragment
|
||||
)
|
||||
|
||||
// Play Now doesn't get a Toast :)
|
||||
if (playNext) {
|
||||
successString = fragment.resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_play_next,
|
||||
songs.size,
|
||||
songs.size
|
||||
)
|
||||
} else if (append) {
|
||||
successString = fragment.resources.getQuantityString(
|
||||
R.plurals.select_album_n_songs_added,
|
||||
songs.size,
|
||||
songs.size
|
||||
)
|
||||
successString = when (insertionMode) {
|
||||
MediaPlayerManager.InsertionMode.AFTER_CURRENT ->
|
||||
fragment.resources.getQuantityString(
|
||||
R.plurals.n_songs_added_after_current,
|
||||
songs.size,
|
||||
songs.size
|
||||
)
|
||||
MediaPlayerManager.InsertionMode.APPEND ->
|
||||
fragment.resources.getQuantityString(
|
||||
R.plurals.n_songs_added_to_end,
|
||||
songs.size,
|
||||
songs.size
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}) { successString }
|
||||
@ -146,8 +147,7 @@ class DownloadHandler(
|
||||
|
||||
fun addTracksToMediaController(
|
||||
songs: List<Track>,
|
||||
append: Boolean,
|
||||
playNext: Boolean,
|
||||
insertionMode: MediaPlayerManager.InsertionMode,
|
||||
autoPlay: Boolean,
|
||||
shuffle: Boolean = false,
|
||||
playlistName: String? = null,
|
||||
@ -157,12 +157,6 @@ class DownloadHandler(
|
||||
|
||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||
|
||||
val insertionMode = when {
|
||||
append -> MediaPlayerManager.InsertionMode.APPEND
|
||||
playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT
|
||||
else -> MediaPlayerManager.InsertionMode.CLEAR
|
||||
}
|
||||
|
||||
if (playlistName != null) {
|
||||
mediaPlayerManager.suggestedPlaylistName = playlistName
|
||||
}
|
||||
@ -173,7 +167,10 @@ class DownloadHandler(
|
||||
shuffle,
|
||||
insertionMode
|
||||
)
|
||||
if (Settings.shouldTransitionOnPlayback && (!append || autoPlay)) {
|
||||
|
||||
if (Settings.shouldTransitionOnPlayback &&
|
||||
insertionMode == MediaPlayerManager.InsertionMode.CLEAR
|
||||
) {
|
||||
fragment.findNavController().popBackStack(R.id.playerFragment, true)
|
||||
fragment.findNavController().navigate(R.id.playerFragment)
|
||||
}
|
||||
|
@ -14,7 +14,8 @@ import androidx.core.net.toUri
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MUSIC
|
||||
import androidx.media3.common.StarRating
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
@ -22,7 +23,7 @@ import java.util.Date
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.provider.AlbumArtContentProvider
|
||||
|
||||
// Copied from androidx.media.utils.MediaConstants in order to avoid importing a whole dependecy
|
||||
// Copied from androidx.media.utils.MediaConstants in order to avoid importing a whole dependency
|
||||
// for a single string value
|
||||
private const val DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE =
|
||||
"android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"
|
||||
@ -76,15 +77,16 @@ fun Track.toMediaItem(
|
||||
title = title ?: "",
|
||||
mediaId = mediaId,
|
||||
isPlayable = !isDirectory,
|
||||
folderType = if (isDirectory) MediaMetadata.FOLDER_TYPE_TITLES
|
||||
else MediaMetadata.FOLDER_TYPE_NONE,
|
||||
isBrowsable = isDirectory,
|
||||
album = album,
|
||||
artist = artist,
|
||||
genre = genre,
|
||||
sourceUri = uri.toUri(),
|
||||
imageUri = artworkUri,
|
||||
starred = starred,
|
||||
group = null
|
||||
group = null,
|
||||
mediaType = if (isDirectory) MEDIA_TYPE_FOLDER_MIXED
|
||||
else MEDIA_TYPE_MUSIC
|
||||
)
|
||||
|
||||
val metadataBuilder = mediaItem.mediaMetadata.buildUpon()
|
||||
@ -204,14 +206,6 @@ private fun safeParseDate(created: String?): Date? {
|
||||
} else null
|
||||
}
|
||||
|
||||
fun MediaItem.setPin(pin: Boolean) {
|
||||
this.mediaMetadata.extras?.putBoolean("pin", pin)
|
||||
}
|
||||
|
||||
fun MediaItem.shouldBePinned(): Boolean {
|
||||
return this.mediaMetadata.extras?.getBoolean("pin") ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new MediaItem from a list of attributes.
|
||||
* Especially useful to create folder entries in the Auto interface.
|
||||
@ -222,7 +216,7 @@ fun buildMediaItem(
|
||||
title: String,
|
||||
mediaId: String,
|
||||
isPlayable: Boolean,
|
||||
folderType: @MediaMetadata.FolderType Int,
|
||||
isBrowsable: Boolean = false,
|
||||
album: String? = null,
|
||||
artist: String? = null,
|
||||
genre: String? = null,
|
||||
@ -241,17 +235,13 @@ fun buildMediaItem(
|
||||
.setAlbumArtist(artist)
|
||||
.setGenre(genre)
|
||||
.setUserRating(HeartRating(starred))
|
||||
.setFolderType(folderType)
|
||||
.setIsBrowsable(isBrowsable)
|
||||
.setIsPlayable(isPlayable)
|
||||
|
||||
if (imageUri != null) {
|
||||
metadataBuilder.setArtworkUri(imageUri)
|
||||
}
|
||||
|
||||
if (folderType > FOLDER_TYPE_NONE) {
|
||||
metadataBuilder.setIsBrowsable(true)
|
||||
}
|
||||
|
||||
if (mediaType != null) {
|
||||
metadataBuilder.setMediaType(mediaType)
|
||||
}
|
||||
|
@ -318,7 +318,7 @@
|
||||
<string name="server_menu.move_down">Posunout níž</string>
|
||||
<string name="server_editor.authentication">Ověření</string>
|
||||
<string name="server_editor.advanced">Rozšířené možnosti</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d skladba</item>
|
||||
<item quantity="few">%d skladby</item>
|
||||
<item quantity="many">%d skladeb</item>
|
||||
|
@ -383,31 +383,31 @@
|
||||
<string name="server_menu.demo">Demo Server</string>
|
||||
<string name="about.webpage">Website besuchen</string>
|
||||
<string name="about.report">Einen Fehler melden</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d Titel</item>
|
||||
<item quantity="other">%d Titel</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d Titel zum Anheften ausgewählt</item>
|
||||
<item quantity="other">%d Titel zum Anheften ausgewählt</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d Titel zum herunterladen ausgewählt</item>
|
||||
<item quantity="other">%d Titel zum herunterladen ausgewählt</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d Titel losgelöst</item>
|
||||
<item quantity="other">%d Titel losgelöst</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d Titel gelöscht</item>
|
||||
<item quantity="other">%d Titel gelöscht</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d Titel am Ende hinzugefügt</item>
|
||||
<item quantity="other">%d Titel am Ende hinzugefügt</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d Titel nach aktuellen Titel hinzugefügt</item>
|
||||
<item quantity="other">%d Titel nach aktuellen Titel hinzugefügt</item>
|
||||
</plurals>
|
||||
|
@ -390,37 +390,37 @@
|
||||
<string name="about.webpage">Visitar la página web</string>
|
||||
<string name="about.report">Informar de un error</string>
|
||||
<string name="about.text"><b>Ultrasonic</b> es un cliente Android de streaming de música gratuito y de código abierto para servidores compatibles con la API de Subsonic (versión 1.7.0 o superior).\n\nCon <b>Ultrasonic</b> puede transmitir o descargar fácilmente música desde su ordenador de casa a su teléfono Android utilizando su servidor multimedia compatible con Subsonic. El software del servidor Subsonic requiere una configuración adicional aparte de Ultrasonic.\n\nPor defecto, Ultrasonic no está configurado. Una vez que hayas configurado tu propio servidor, cambia la configuración del servidor para que se conecte a tu propio ordenador.</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d canción</item>
|
||||
<item quantity="many">%d canciones</item>
|
||||
<item quantity="other">%d canciones</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d canción seleccionada para ser anclada</item>
|
||||
<item quantity="many">%d canciones seleccionadas para ser ancladas</item>
|
||||
<item quantity="other">%d canciones seleccionadas para ser ancladas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d canción seleccionada para ser descargada</item>
|
||||
<item quantity="many">%d canciones seleccionadas para ser descargadas</item>
|
||||
<item quantity="other">%d canciones seleccionadas para ser descargadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d canción desanclada</item>
|
||||
<item quantity="many">%d canciones desancladas</item>
|
||||
<item quantity="other">%d canciones desancladas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d canción eliminada</item>
|
||||
<item quantity="many">%d canciones eliminadas</item>
|
||||
<item quantity="other">%d canciones eliminadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d canción añadida al final de la cola de reproducción</item>
|
||||
<item quantity="many">%d canciones añadidas al final de la cola de reproducción</item>
|
||||
<item quantity="other">%d canciones añadidas al final de la cola de reproducción</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d canción insertada después de la canción actual</item>
|
||||
<item quantity="many">%d canciones insertadas después de la canción actual</item>
|
||||
<item quantity="other">%d canciones insertadas después de la canción actual</item>
|
||||
|
@ -372,7 +372,7 @@
|
||||
<string name="about.webpage">Visiter la page web</string>
|
||||
<string name="about.report">Signaler un bug</string>
|
||||
<string name="about.text"><b>Ultrasonic</b> est un client Android de streaming musical gratuit et open-source pour les serveurs compatibles avec l\'API Subsonic (version 1.7.0 ou supérieure). Avec <b>Ultrasonic</b>, vous pouvez facilement diffuser ou télécharger de la musique depuis votre ordinateur personnel vers votre téléphone Android en utilisant votre serveur multimédia compatible Subsonic. Le logiciel du serveur Subsonic nécessite une configuration supplémentaire distincte d\'Ultrasonic. Par défaut, Ultrasonic n\'est pas configuré. Une fois que vous avez mis en place votre propre serveur, veuillez modifier la configuration du serveur afin qu\'il se connecte à votre ordinateur.</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d titre</item>
|
||||
<item quantity="many">%d titres</item>
|
||||
<item quantity="other">%d titres</item>
|
||||
@ -400,12 +400,12 @@
|
||||
<string name="settings.preload_50">50 morceaux</string>
|
||||
<string name="chat.user_avatar">Image d\'avatar</string>
|
||||
<string name="settings.theme_day_night">Jour et nuit</string>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d morceau ajouté à la file d\'attente de fin de lecture</item>
|
||||
<item quantity="many">"%d morceaux ajoutés à la file d\'attente de fin de lecture"</item>
|
||||
<item quantity="other">%d morceaux ajoutés à la file d\'attente de fin de lecture</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d morceau supprimé</item>
|
||||
<item quantity="many">%d morceaux supprimés</item>
|
||||
<item quantity="other">%d morceaux supprimés</item>
|
||||
@ -429,22 +429,22 @@
|
||||
<string name="settings.parallel_downloads">Combien de chansons peuvent être téléchargées en parallèle</string>
|
||||
<string name="settings.show_now_playing_details_summary">Afficher plus de détails sur la chanson dans la lecture en cours (genre, année, débit)</string>
|
||||
<string name="list_view">Liste</string>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d chanson sélectionnée pour téléchargement</item>
|
||||
<item quantity="many">%d chansons sélectionnées pour téléchargement</item>
|
||||
<item quantity="other">%d chansons sélectionnées pour téléchargement</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d chanson désépinglée</item>
|
||||
<item quantity="many">%d chansons désépinglées</item>
|
||||
<item quantity="other">%d chansons désépinglées</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d chanson insérée après la chanson en cours</item>
|
||||
<item quantity="many">%d chansons insérées après la chanson en cours</item>
|
||||
<item quantity="other">%d chansons insérées après la chanson en cours</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d chanson sélectionnée à épingler</item>
|
||||
<item quantity="many">%d chansons sélectionnées à épingler</item>
|
||||
<item quantity="other">%d chansons sélectionnées à épingler</item>
|
||||
|
@ -51,12 +51,12 @@
|
||||
<string name="common.play_last">Reproducir última</string>
|
||||
<string name="common.pin">Ancorar</string>
|
||||
<string name="settings.show_confirmation_dialog_summary">Mostra un cadro de diálogo de confirmación antes de eliminar ou desancorar as cancións</string>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d canción seleccionada para ser ancorada</item>
|
||||
<item quantity="other">%d cancións seleccionadas para ser ancoradas</item>
|
||||
</plurals>
|
||||
<string name="settings.max_bitrate_pinning">Tasa de bits máxima: ao fixar unha canción de forma permanente</string>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d canción desancorada</item>
|
||||
<item quantity="other">%d cancións desancoradas</item>
|
||||
</plurals>
|
||||
|
@ -326,7 +326,7 @@
|
||||
<string name="server_menu.move_down">Lejjebb mozgat</string>
|
||||
<string name="server_editor.authentication">Bejelentkezés</string>
|
||||
<string name="server_editor.advanced">Haladó beállítások</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d dal</item>
|
||||
<item quantity="other">%d dal</item>
|
||||
</plurals>
|
||||
|
@ -302,25 +302,25 @@
|
||||
<string name="server_menu.demo">デモサーバー</string>
|
||||
<string name="about.webpage">Webページにアクセス</string>
|
||||
<string name="about.report">バグを報告</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="other">%d 曲</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="other">%d 曲がダウンロード選択されました</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="other">%d 曲が固定解除されました</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="other">%d 曲が固定されるよう選択されました</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="other">%d 曲が削除されました</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="other">%d 曲が再生キューの末尾に追加されました</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="other">%d 曲が現在再生中の曲の次に追加されました</item>
|
||||
</plurals>
|
||||
<string name="api.subsonic.generic">一般APIエラー: %1$s</string>
|
||||
|
@ -123,11 +123,11 @@
|
||||
<string name="share_set_share_options">Sett delingsinnstillinger</string>
|
||||
<string name="button_bar.shares">Delinger</string>
|
||||
<string name="download.toggle_playlist">Veksle spilleliste</string>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d spor lagt til etter nåværende spor</item>
|
||||
<item quantity="other">%d spor lagt til etter nåværende spor</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d spor lagt til på slutten av spillekøen</item>
|
||||
<item quantity="other">%d spor lagt til på slutten av spillekøen</item>
|
||||
</plurals>
|
||||
@ -290,19 +290,19 @@
|
||||
<string name="settings.debug.log_to_file">Skriv avlusningslogg til fil</string>
|
||||
<string name="about.report">Rapporter en feil</string>
|
||||
<string name="api.subsonic.generic">Generisk API-feil: %1$s</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d spor</item>
|
||||
<item quantity="other">%d spor</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d spor å feste</item>
|
||||
<item quantity="other">%d spor å feste</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d spor løsnet</item>
|
||||
<item quantity="other">%d spor løsnet</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d spor slettet</item>
|
||||
<item quantity="other">%d spor løsnet</item>
|
||||
</plurals>
|
||||
@ -446,7 +446,7 @@
|
||||
<string name="grid_view">Omslag</string>
|
||||
<string name="supported_server_features">Støttede funksjoner</string>
|
||||
<string name="jukebox">Jukebox</string>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d spor valgt for nedlasting</item>
|
||||
<item quantity="other">%d spor valgt for nedlasting</item>
|
||||
</plurals>
|
||||
|
@ -391,31 +391,31 @@
|
||||
<string name="about.webpage">Website openen</string>
|
||||
<string name="about.report">Bug melden</string>
|
||||
<string name="about.text"><b>Ultrasonic</b> is een gratis, open source muziekstreamingclient voor Android, die gebruikmaakt van servers die compatibel zijn met de Subsonic-api (versie 1.7.0 of hoger).\n\nMet <b>Ultrasonic</b> kun je eenvoudig muziek streamen of downloaden van je computer naar je Android-telefoon met behulp van een met Subsonic compatibele mediaserver. Let op: de Subsonic-serversoftware vereist aanvullende configuratie.\n\nStandaard is Ultrasonic niet ingesteld. Zet je eigen server op en wijzig de serverconfiguratie in die van je eigen.</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d nummer</item>
|
||||
<item quantity="other">%d nummers</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d vast te maken nummer geselecteerd</item>
|
||||
<item quantity="other">%d vast te maken nummers geselecteerd</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d te downloaden nummer geselecteerd</item>
|
||||
<item quantity="other">%d te downloaden nummers geselecteerd</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d nummer losgemaakt</item>
|
||||
<item quantity="other">%d nummers losgemaakt</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d nummer verwijderd</item>
|
||||
<item quantity="other">%d nummers verwijderd</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d nummer toegevoegd aan het einde van afspeelwachtrij</item>
|
||||
<item quantity="other">%d nummers toegevoegd aan het einde van afspeelwachtrij</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d nummer ingevoegd na het huidige nummer</item>
|
||||
<item quantity="other">%d nummers ingevoegd na het huidige nummer</item>
|
||||
</plurals>
|
||||
|
@ -307,7 +307,7 @@
|
||||
<string name="server_menu.move_down">Przesuń się w dół</string>
|
||||
<string name="server_editor.authentication">Authentication</string>
|
||||
<string name="server_editor.advanced">Ustawienia zaawansowane</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d utwór</item>
|
||||
<item quantity="few">%d utwory</item>
|
||||
<item quantity="many">%d utworów</item>
|
||||
@ -356,7 +356,7 @@
|
||||
<string name="language.hu">Węgierski</string>
|
||||
<string name="settings.debug.log_summary">Zapisanych jest %1$s plików z logami, które zajmują ~%2$s MB miejsca w katalogu %3$s. Czy chcesz je zachować\?</string>
|
||||
<string name="buttons.previous">Poprzednie</string>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">Usunięto %d utwór</item>
|
||||
<item quantity="few">Usunięto %d utwory</item>
|
||||
<item quantity="many">Usunięto %d utworów</item>
|
||||
@ -403,20 +403,20 @@
|
||||
<string name="settings.wifi_required_title">Pobieraj tylko przez Wi-Fi</string>
|
||||
<string name="settings.sharing_always_ask_for_details_summary">Zawsze pytaj o opis i czas wygaśnięcia podczas tworzenia udostępnienia na serwerze</string>
|
||||
<string name="settings.debug.log_delete">Usuń pliki</string>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d utwór zaznaczony do przypięcia</item>
|
||||
<item quantity="few">%d utwory zaznaczone do przypięcia</item>
|
||||
<item quantity="many">%d utworów zaznaczonych do przypięcia</item>
|
||||
<item quantity="other">%d utworów zaznaczonych do przypięcia</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d utworów zaznaczonych do pobrania</item>
|
||||
<item quantity="few">%d utwory zaznaczone do pobrania</item>
|
||||
<item quantity="many">%d utworów zaznaczonych do pobrania</item>
|
||||
<item quantity="other">%d utworów zaznaczonych do pobrania</item>
|
||||
</plurals>
|
||||
<string name="about.webpage">Odwiedź stronę internetową</string>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">Odpięto %d utwór</item>
|
||||
<item quantity="few">Odpięto %d utwory</item>
|
||||
<item quantity="many">Odpięto %d utworów</item>
|
||||
@ -446,13 +446,13 @@
|
||||
<string name="list_view">Lista</string>
|
||||
<string name="grid_view">Okładka</string>
|
||||
<string name="settings.use_hw_offload_description">Spróbuj odtworzyć pliki multimedialne za pomocą układu dekodującego w telefonie. Może to zmniejszyć zużycie baterii!</string>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">Dodano %d utwór na koniec kolejki odtwarzania</item>
|
||||
<item quantity="few">Dodano %d utwory na koniec kolejki odtwarzania</item>
|
||||
<item quantity="many">Dodano %d utworów na koniec kolejki odtwarzania</item>
|
||||
<item quantity="other">Dodano %d utworów na koniec kolejki odtwarzania</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">Wstawiono %d utwór po bieżącym utworze</item>
|
||||
<item quantity="few">Wstawiono %d utwory po bieżącym utworze</item>
|
||||
<item quantity="many">Wstawiono %d utworów po bieżącym utworze</item>
|
||||
|
@ -388,37 +388,37 @@
|
||||
<string name="about.webpage">Visitar a página web</string>
|
||||
<string name="about.report">Reportar um erro</string>
|
||||
<string name="about.text"><b>Ultrasonic</b> é um cliente gratuito e open-source para Android de streaming de música para API de servidores compatíveis com Subsonic (version 1.7.0 ou maior).\n\nCom <b>Ultrasonic</b> você pode facilmente reproduzir online ou baixar música de seu computador doméstico para seu telefone Android usando um servidor de mídia compatível com Subsonic. O software do servidor Subsonic necessita uma configuração independente do Ultrasonic.\n\nPor padrão, o servidor Ultrasonic não é configurado. Uma vez que você configurou seu próprio servidor, altere a configuração no Ultrasonic para poder conectá-lo.</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d música</item>
|
||||
<item quantity="many">%d músicas</item>
|
||||
<item quantity="other">%d músicas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d música selecionada para ser fixada</item>
|
||||
<item quantity="many">%d músicas selecionadas para serem fixadas</item>
|
||||
<item quantity="other">%d músicas selecionadas para serem fixadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d música selecionada para ser baixada</item>
|
||||
<item quantity="many">%d músicas selecionadas para serem baixadas</item>
|
||||
<item quantity="other">%d músicas selecionadas para serem baixadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d música desfixada</item>
|
||||
<item quantity="many">%d músicas desfixadas</item>
|
||||
<item quantity="other">%d músicas desfixadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d música excluída</item>
|
||||
<item quantity="many">%d músicas excluídas</item>
|
||||
<item quantity="other">%d músicas excluídas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d música adicionada ao final da playlist</item>
|
||||
<item quantity="many">%d músicas adicionadas ao final da playlist</item>
|
||||
<item quantity="other">%d músicas adicionadas ao final da playlist</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d música adicionada após a atual</item>
|
||||
<item quantity="many">%d músicas adicionadas após a atual</item>
|
||||
<item quantity="other">%d músicas adicionadas após a atual</item>
|
||||
|
@ -307,7 +307,7 @@
|
||||
<string name="server_menu.move_down">Move down</string>
|
||||
<string name="server_editor.authentication">Authentication</string>
|
||||
<string name="server_editor.advanced">Configurações avançadas</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d música</item>
|
||||
<item quantity="many">%d músicas</item>
|
||||
<item quantity="other">%d músicas</item>
|
||||
@ -330,22 +330,22 @@
|
||||
<string name="server_menu.demo">Servidor Demonstração</string>
|
||||
<string name="about.webpage">Visitar a página web</string>
|
||||
<string name="about.report">Reportar um erro</string>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d música selecionada para ser fixada</item>
|
||||
<item quantity="many">%d músicas selecionadas para serem fixadas</item>
|
||||
<item quantity="other">%d músicas selecionadas para serem fixadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d música desfixada</item>
|
||||
<item quantity="many">%d músicas desfixadas</item>
|
||||
<item quantity="other">%d músicas desfixadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d música excluída</item>
|
||||
<item quantity="many">%d músicas excluídas</item>
|
||||
<item quantity="other">%d músicas excluídas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d música adicionada ao final da playlist</item>
|
||||
<item quantity="many">%d músicas adicionadas ao final da playlist</item>
|
||||
<item quantity="other">%d músicas adicionadas ao final da playlist</item>
|
||||
@ -447,12 +447,12 @@
|
||||
\nCom <b>Ultrasonic</b>, pode facilmente reproduzir online ou descarregar música do seu computador doméstico para o seu telefone Android usando um servidor de mídia compatível com Subsonic. O software do servidor Subsonic necessita uma configuração independente do Ultrasonic.
|
||||
\n
|
||||
\nPor padrão, o servidor Ultrasonic não é configurado. Uma vez que configurou o seu próprio servidor, altere a configuração no Ultrasonic para poder conectá-lo.</string>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d música selecionada a ser descarregada</item>
|
||||
<item quantity="many">%d músicas selecionadas para serem baixadas</item>
|
||||
<item quantity="other">%d músicas selecionadas para serem baixadas</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d música adicionada após a atual</item>
|
||||
<item quantity="many">%d músicas adicionadas após a atual</item>
|
||||
<item quantity="other">%d músicas adicionadas após a atual</item>
|
||||
|
@ -347,7 +347,7 @@
|
||||
<string name="server_editor.disabled_feature">Одна или несколько функций были отключены, потому что сервер их не поддерживает.\nВы можете запустить этот тест снова в любое время.</string>
|
||||
<string name="server_menu.demo">Демо-сервер</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d песня</item>
|
||||
<item quantity="few">%d песни</item>
|
||||
<item quantity="many">%d песен</item>
|
||||
|
@ -368,25 +368,25 @@
|
||||
\n通过使用 <b>Ultrasonic</b> 你可以轻松的从你的电脑上的 Subsonic 兼容服务端流式传输或者下载音乐。 Subsonic 服务端与 Ultrasonic 都需要额外的配置才可使用。
|
||||
\n
|
||||
\n默认情况下,Ultrasonic 并未进行配置,当服务端配置完成后,请确保配置允许客户端连接到你的计算机。</string>
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="other">%d 首曲目</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="other">已选择 %d 首歌曲进行固定</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="other">已选择要下载 %d 首歌曲</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="other">已选择 %d 首歌曲取消固定</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="other">%d 首歌曲被删除</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="other">已将 %d 首歌曲添加到播放队列的末尾</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="other">在当前歌曲之后插入了 %d 首歌曲</item>
|
||||
</plurals>
|
||||
<!-- Subsonic api errors -->
|
||||
|
@ -402,31 +402,35 @@
|
||||
<string name="about.webpage.url" translatable="false">https://ultrasonic.gitlab.io/</string>
|
||||
<string name="about.report.url" translatable="false">https://gitlab.com/ultrasonic/ultrasonic/issues</string>
|
||||
|
||||
<plurals name="select_album_n_songs">
|
||||
<plurals name="n_songs">
|
||||
<item quantity="one">%d song</item>
|
||||
<item quantity="other">%d songs</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_pinned">
|
||||
<plurals name="n_songs_pinned">
|
||||
<item quantity="one">%d song selected to be pinned</item>
|
||||
<item quantity="other">%d songs selected to be pinned</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_downloaded">
|
||||
<plurals name="n_songs_to_be_downloaded">
|
||||
<item quantity="one">%d song selected to be downloaded</item>
|
||||
<item quantity="other">%d songs selected to be downloaded</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_unpinned">
|
||||
<plurals name="n_songs_unpinned">
|
||||
<item quantity="one">%d song unpinned</item>
|
||||
<item quantity="other">%d songs unpinned</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_deleted">
|
||||
<plurals name="n_songs_deleted">
|
||||
<item quantity="one">%d song deleted</item>
|
||||
<item quantity="other">%d songs deleted</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_added">
|
||||
<plurals name="n_songs_added_play_now">
|
||||
<item quantity="one">%d song added to the play queue</item>
|
||||
<item quantity="other">%d songs added to the play queue</item>
|
||||
</plurals>
|
||||
<plurals name="n_songs_added_to_end">
|
||||
<item quantity="one">%d song added to the end of play queue</item>
|
||||
<item quantity="other">%d songs added to the end of play queue</item>
|
||||
</plurals>
|
||||
<plurals name="select_album_n_songs_play_next">
|
||||
<plurals name="n_songs_added_after_current">
|
||||
<item quantity="one">%d song inserted after current song</item>
|
||||
<item quantity="other">%d songs inserted after current song</item>
|
||||
</plurals>
|
||||
|
Loading…
x
Reference in New Issue
Block a user