mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-01 16:11:13 +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:
|
script:
|
||||||
- curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
|
- 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 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:
|
rules:
|
||||||
# Run when releasing a new tag
|
# Run when releasing a new tag
|
||||||
- if: $CI_COMMIT_TAG && $CI_PROJECT_ID == $ROOT_PROJECT_ID
|
- 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.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
buildscript {
|
buildscript {
|
||||||
apply from: 'gradle/versions.gradle'
|
apply from: 'gradle/versions.gradle'
|
||||||
@ -10,6 +12,7 @@ buildscript {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
maven { url "https://plugins.gradle.org/m2/" }
|
maven { url "https://plugins.gradle.org/m2/" }
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
@ -26,21 +29,29 @@ allprojects {
|
|||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set Kotlin JVM target to the same for all subprojects
|
// Set Kotlin JVM target to the same for all subprojects
|
||||||
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.withType(JavaCompile).tap {
|
||||||
|
configureEach {
|
||||||
|
options.compilerArgs.add("-Xlint:deprecation")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper {
|
wrapper {
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
|
plugins {
|
||||||
|
alias libs.plugins.ksp
|
||||||
|
}
|
||||||
|
|
||||||
apply from: bootstrap.androidModule
|
apply from: bootstrap.androidModule
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation libs.core
|
implementation libs.core
|
||||||
implementation libs.roomRuntime
|
implementation libs.roomRuntime
|
||||||
implementation libs.roomKtx
|
implementation libs.roomKtx
|
||||||
kapt libs.room
|
ksp libs.room
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
plugins {
|
||||||
|
alias libs.plugins.ksp
|
||||||
|
}
|
||||||
|
|
||||||
apply from: bootstrap.kotlinModule
|
apply from: bootstrap.kotlinModule
|
||||||
|
|
||||||
dependencies {
|
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"
|
gradle = "8.1.1"
|
||||||
|
|
||||||
navigation = "2.6.0"
|
navigation = "2.6.0"
|
||||||
gradlePlugin = "8.0.2"
|
gradlePlugin = "8.1.0"
|
||||||
androidxcore = "1.10.1"
|
androidxcore = "1.10.1"
|
||||||
ktlint = "0.43.2"
|
ktlint = "0.43.2"
|
||||||
ktlintGradle = "11.4.2"
|
ktlintGradle = "11.5.0"
|
||||||
detekt = "1.23.0"
|
detekt = "1.23.0"
|
||||||
preferences = "1.2.0"
|
preferences = "1.2.1"
|
||||||
media3 = "1.0.2"
|
media3 = "1.1.0"
|
||||||
|
|
||||||
androidSupport = "1.6.0"
|
androidSupport = "1.6.0"
|
||||||
materialDesign = "1.9.0"
|
materialDesign = "1.9.0"
|
||||||
@ -17,7 +17,8 @@ constraintLayout = "2.1.4"
|
|||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
room = "2.5.2"
|
room = "2.5.2"
|
||||||
kotlin = "1.8.22"
|
kotlin = "1.8.22"
|
||||||
kotlinxCoroutines = "1.7.1"
|
ksp = "1.8.22-1.0.11"
|
||||||
|
kotlinxCoroutines = "1.7.3"
|
||||||
viewModelKtx = "2.6.1"
|
viewModelKtx = "2.6.1"
|
||||||
swipeRefresh = "1.1.0"
|
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
|
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
||||||
jackson = "2.13.5"
|
jackson = "2.13.5"
|
||||||
okhttp = "4.11.0"
|
okhttp = "4.11.0"
|
||||||
koin = "3.3.2"
|
koin = "3.4.3"
|
||||||
picasso = "2.8"
|
picasso = "2.8"
|
||||||
|
|
||||||
junit4 = "4.13.2"
|
junit4 = "4.13.2"
|
||||||
junit5 = "5.9.3"
|
junit5 = "5.10.0"
|
||||||
mockito = "5.4.0"
|
mockito = "5.4.0"
|
||||||
mockitoKotlin = "5.0.0"
|
mockitoKotlin = "5.0.0"
|
||||||
kluent = "1.73"
|
kluent = "1.73"
|
||||||
@ -100,3 +101,6 @@ kluentAndroid = { module = "org.amshove.kluent:kluent-android", versio
|
|||||||
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||||
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
apacheCodecs = { module = "commons-codec:commons-codec", version.ref = "apacheCodecs" }
|
||||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
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
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
apply plugin: 'com.android.library'
|
apply plugin: 'com.android.library'
|
||||||
apply plugin: 'org.jetbrains.kotlin.android'
|
apply plugin: 'org.jetbrains.kotlin.android'
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
apply plugin: 'org.jetbrains.kotlin.kapt'
|
apply plugin: 'com.google.devtools.ksp'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion versions.compileSdk
|
compileSdkVersion versions.compileSdk
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
* This module provides a base for for pure kotlin modules
|
* This module provides a base for for pure kotlin modules
|
||||||
*/
|
*/
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
apply plugin: 'org.jetbrains.kotlin.kapt'
|
apply plugin: 'com.google.devtools.ksp'
|
||||||
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
apply from: "${project.rootDir}/gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
@ -12,7 +12,6 @@ sourceSets {
|
|||||||
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
|
test.resources.srcDirs += "${projectDir}/src/integrationTest/resources"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.kotlinStdlib
|
api libs.kotlinStdlib
|
||||||
|
|
||||||
|
5
gradlew
vendored
5
gradlew
vendored
@ -130,10 +130,13 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
plugins {
|
||||||
|
alias libs.plugins.ksp
|
||||||
|
}
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'org.jetbrains.kotlin.android'
|
apply plugin: 'org.jetbrains.kotlin.android'
|
||||||
apply plugin: 'org.jetbrains.kotlin.kapt'
|
|
||||||
apply plugin: "androidx.navigation.safeargs.kotlin"
|
apply plugin: "androidx.navigation.safeargs.kotlin"
|
||||||
apply from: "../gradle_scripts/code_quality.gradle"
|
apply from: "../gradle_scripts/code_quality.gradle"
|
||||||
|
|
||||||
@ -9,8 +12,8 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "org.moire.ultrasonic"
|
applicationId "org.moire.ultrasonic"
|
||||||
versionCode 126
|
versionCode 128
|
||||||
versionName "4.6.3"
|
versionName "4.7.0"
|
||||||
|
|
||||||
minSdkVersion versions.minSdk
|
minSdkVersion versions.minSdk
|
||||||
targetSdkVersion versions.targetSdk
|
targetSdkVersion versions.targetSdk
|
||||||
@ -64,20 +67,20 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kapt {
|
ksp {
|
||||||
arguments {
|
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas")
|
||||||
arg("room.schemaLocation", "$rootDir/ultrasonic/schemas".toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
baseline = file("lint-baseline.xml")
|
baseline = file("lint-baseline.xml")
|
||||||
abortOnError true
|
abortOnError true
|
||||||
warningsAsErrors true
|
warningsAsErrors true
|
||||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
|
||||||
ignore 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
|
||||||
warning 'ImpliedQuantity'
|
warning 'ImpliedQuantity'
|
||||||
|
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||||
|
disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||||
disable 'ObsoleteLintCustomCheck'
|
disable 'ObsoleteLintCustomCheck'
|
||||||
|
// We manage dependencies on Gitlab with RenovateBot
|
||||||
|
disable 'GradleDependency'
|
||||||
textReport true
|
textReport true
|
||||||
checkDependencies true
|
checkDependencies true
|
||||||
}
|
}
|
||||||
@ -85,7 +88,7 @@ android {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType(Test) {
|
tasks.withType(Test).configureEach {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,7 +132,7 @@ dependencies {
|
|||||||
implementation libs.rxAndroid
|
implementation libs.rxAndroid
|
||||||
implementation libs.multiType
|
implementation libs.multiType
|
||||||
|
|
||||||
kapt libs.room
|
ksp libs.room
|
||||||
|
|
||||||
testImplementation libs.kotlinReflect
|
testImplementation libs.kotlinReflect
|
||||||
testImplementation libs.junit
|
testImplementation libs.junit
|
||||||
@ -141,6 +144,5 @@ dependencies {
|
|||||||
testImplementation libs.robolectric
|
testImplementation libs.robolectric
|
||||||
|
|
||||||
implementation libs.timber
|
implementation libs.timber
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -488,8 +488,7 @@ class NavigationActivity : AppCompatActivity() {
|
|||||||
val downloadHandler: DownloadHandler by inject()
|
val downloadHandler: DownloadHandler by inject()
|
||||||
downloadHandler.addTracksToMediaController(
|
downloadHandler.addTracksToMediaController(
|
||||||
songs = musicDirectory.getTracks(),
|
songs = musicDirectory.getTracks(),
|
||||||
append = false,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
playNext = false,
|
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
fragment = currentFragment,
|
fragment = currentFragment,
|
||||||
|
@ -98,7 +98,7 @@ class HeaderViewBinder(
|
|||||||
holder.yearView.text = year
|
holder.yearView.text = year
|
||||||
|
|
||||||
val songs = resources.getQuantityString(
|
val songs = resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs, item.childCount,
|
R.plurals.n_songs, item.childCount,
|
||||||
item.childCount
|
item.childCount
|
||||||
)
|
)
|
||||||
holder.songCountView.text = songs
|
holder.songCountView.text = songs
|
||||||
|
@ -56,7 +56,7 @@ class BookmarksFragment : TrackCollectionFragment() {
|
|||||||
super.setupButtons(view)
|
super.setupButtons(view)
|
||||||
|
|
||||||
playNowButton!!.setOnClickListener {
|
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.Artist
|
||||||
import org.moire.ultrasonic.domain.GenericEntry
|
import org.moire.ultrasonic.domain.GenericEntry
|
||||||
import org.moire.ultrasonic.domain.Identifiable
|
import org.moire.ultrasonic.domain.Identifiable
|
||||||
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
import org.moire.ultrasonic.subsonic.DownloadAction
|
||||||
@ -133,27 +134,24 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>() {
|
|||||||
downloadHandler.fetchTracksAndAddToController(
|
downloadHandler.fetchTracksAndAddToController(
|
||||||
fragment,
|
fragment,
|
||||||
item.id,
|
item.id,
|
||||||
append = false,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
playNext = false,
|
|
||||||
isArtist = isArtist
|
isArtist = isArtist
|
||||||
)
|
)
|
||||||
R.id.menu_play_next ->
|
R.id.menu_play_next ->
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
downloadHandler.fetchTracksAndAddToController(
|
||||||
fragment,
|
fragment,
|
||||||
item.id,
|
item.id,
|
||||||
append = false,
|
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
playNext = true,
|
|
||||||
isArtist = isArtist
|
isArtist = isArtist
|
||||||
)
|
)
|
||||||
R.id.menu_play_last ->
|
R.id.menu_play_last ->
|
||||||
downloadHandler.fetchTracksAndAddToController(
|
downloadHandler.fetchTracksAndAddToController(
|
||||||
fragment,
|
fragment,
|
||||||
item.id,
|
item.id,
|
||||||
append = true,
|
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
|
||||||
autoPlay = false,
|
autoPlay = false,
|
||||||
playNext = false,
|
|
||||||
isArtist = isArtist
|
isArtist = isArtist
|
||||||
)
|
)
|
||||||
R.id.menu_pin ->
|
R.id.menu_pin ->
|
||||||
|
@ -253,7 +253,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
insertionMode = MediaPlayerManager.InsertionMode.APPEND
|
||||||
)
|
)
|
||||||
mediaPlayerManager.play(mediaPlayerManager.mediaItemCount - 1)
|
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) {
|
private fun onVideoSelected(track: Track) {
|
||||||
@ -307,8 +307,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
songs.add(item)
|
songs.add(item)
|
||||||
downloadHandler.addTracksToMediaController(
|
downloadHandler.addTracksToMediaController(
|
||||||
songs = songs,
|
songs = songs,
|
||||||
append = false,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
playNext = false,
|
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
@ -319,8 +318,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
songs.add(item)
|
songs.add(item)
|
||||||
downloadHandler.addTracksToMediaController(
|
downloadHandler.addTracksToMediaController(
|
||||||
songs = songs,
|
songs = songs,
|
||||||
append = true,
|
insertionMode = MediaPlayerManager.InsertionMode.AFTER_CURRENT,
|
||||||
playNext = true,
|
|
||||||
autoPlay = false,
|
autoPlay = false,
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
@ -331,8 +329,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
songs.add(item)
|
songs.add(item)
|
||||||
downloadHandler.addTracksToMediaController(
|
downloadHandler.addTracksToMediaController(
|
||||||
songs = songs,
|
songs = songs,
|
||||||
append = true,
|
insertionMode = MediaPlayerManager.InsertionMode.APPEND,
|
||||||
playNext = false,
|
|
||||||
autoPlay = false,
|
autoPlay = false,
|
||||||
shuffle = false,
|
shuffle = false,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
@ -344,7 +341,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
toast(
|
toast(
|
||||||
context,
|
context,
|
||||||
resources.getQuantityString(
|
resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs_pinned,
|
R.plurals.n_songs_pinned,
|
||||||
songs.size,
|
songs.size,
|
||||||
songs.size
|
songs.size
|
||||||
)
|
)
|
||||||
@ -356,7 +353,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
toast(
|
toast(
|
||||||
context,
|
context,
|
||||||
resources.getQuantityString(
|
resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs_downloaded,
|
R.plurals.n_songs_to_be_downloaded,
|
||||||
songs.size,
|
songs.size,
|
||||||
songs.size
|
songs.size
|
||||||
)
|
)
|
||||||
@ -368,7 +365,7 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinComponent {
|
|||||||
toast(
|
toast(
|
||||||
context,
|
context,
|
||||||
resources.getQuantityString(
|
resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs_unpinned,
|
R.plurals.n_songs_unpinned,
|
||||||
songs.size,
|
songs.size,
|
||||||
songs.size
|
songs.size
|
||||||
)
|
)
|
||||||
|
@ -159,7 +159,7 @@ open class TrackCollectionFragment(
|
|||||||
// Change the buttons if the status of any selected track changes
|
// Change the buttons if the status of any selected track changes
|
||||||
rxBusSubscription += RxBus.trackDownloadStateObservable.subscribe {
|
rxBusSubscription += RxBus.trackDownloadStateObservable.subscribe {
|
||||||
if (it.progress != null) return@subscribe
|
if (it.progress != null) return@subscribe
|
||||||
val selectedSongs = getSelectedSongs()
|
val selectedSongs = getSelectedTracks()
|
||||||
if (!selectedSongs.any { song -> song.id == it.id }) return@subscribe
|
if (!selectedSongs.any { song -> song.id == it.id }) return@subscribe
|
||||||
triggerButtonUpdate(selectedSongs)
|
triggerButtonUpdate(selectedSongs)
|
||||||
}
|
}
|
||||||
@ -211,23 +211,15 @@ open class TrackCollectionFragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
playNowButton?.setOnClickListener {
|
playNowButton?.setOnClickListener {
|
||||||
playNow(false)
|
playNow(MediaPlayerManager.InsertionMode.CLEAR, toast = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
playNextButton?.setOnClickListener {
|
playNextButton?.setOnClickListener {
|
||||||
downloadHandler.addTracksToMediaController(
|
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, toast = true)
|
||||||
songs = getSelectedSongs(),
|
|
||||||
append = true,
|
|
||||||
playNext = true,
|
|
||||||
autoPlay = false,
|
|
||||||
shuffle = false,
|
|
||||||
playlistName = navArgs.playlistName,
|
|
||||||
this@TrackCollectionFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playLastButton!!.setOnClickListener {
|
playLastButton!!.setOnClickListener {
|
||||||
playNow(true)
|
playNow(MediaPlayerManager.InsertionMode.APPEND, toast = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pinButton?.setOnClickListener {
|
pinButton?.setOnClickListener {
|
||||||
@ -291,7 +283,7 @@ open class TrackCollectionFragment(
|
|||||||
return true
|
return true
|
||||||
} else if (item.itemId == R.id.menu_item_share) {
|
} else if (item.itemId == R.id.menu_item_share) {
|
||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this@TrackCollectionFragment, getSelectedSongs(),
|
this@TrackCollectionFragment, getSelectedTracks(),
|
||||||
refreshListView, cancellationToken!!,
|
refreshListView, cancellationToken!!,
|
||||||
navArgs.id
|
navArgs.id
|
||||||
)
|
)
|
||||||
@ -308,20 +300,37 @@ open class TrackCollectionFragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun playNow(
|
private fun playNow(
|
||||||
append: Boolean,
|
insertionMode: MediaPlayerManager.InsertionMode,
|
||||||
selectedSongs: List<Track> = getSelectedSongs()
|
selectedTracks: List<Track> = getSelectedTracks(),
|
||||||
|
toast: Boolean = false
|
||||||
) {
|
) {
|
||||||
if (selectedSongs.isNotEmpty()) {
|
if (selectedTracks.isNotEmpty()) {
|
||||||
downloadHandler.addTracksToMediaController(
|
downloadHandler.addTracksToMediaController(
|
||||||
songs = selectedSongs,
|
songs = selectedTracks,
|
||||||
append = append,
|
insertionMode = insertionMode,
|
||||||
playNext = false,
|
autoPlay = (insertionMode == MediaPlayerManager.InsertionMode.CLEAR),
|
||||||
autoPlay = !append,
|
|
||||||
playlistName = null,
|
playlistName = null,
|
||||||
fragment = this
|
fragment = this
|
||||||
)
|
)
|
||||||
} else {
|
} 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
|
var hasSubFolders = false
|
||||||
|
|
||||||
for (item in viewAdapter.getCurrentList()) {
|
for (item in viewAdapter.getCurrentList()) {
|
||||||
@ -355,18 +367,16 @@ open class TrackCollectionFragment(
|
|||||||
downloadHandler.fetchTracksAndAddToController(
|
downloadHandler.fetchTracksAndAddToController(
|
||||||
fragment = this,
|
fragment = this,
|
||||||
id = navArgs.id!!,
|
id = navArgs.id!!,
|
||||||
append = append,
|
insertionMode = insertionMode,
|
||||||
autoPlay = !append,
|
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||||
shuffle = shuffle,
|
shuffle = shuffle,
|
||||||
playNext = false,
|
|
||||||
isArtist = isArtist
|
isArtist = isArtist
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
downloadHandler.addTracksToMediaController(
|
downloadHandler.addTracksToMediaController(
|
||||||
songs = getAllSongs(),
|
songs = getAllSongs(),
|
||||||
append = append,
|
insertionMode = insertionMode,
|
||||||
playNext = false,
|
autoPlay = (insertionMode != MediaPlayerManager.InsertionMode.APPEND),
|
||||||
autoPlay = !append,
|
|
||||||
shuffle = shuffle,
|
shuffle = shuffle,
|
||||||
playlistName = navArgs.playlistName,
|
playlistName = navArgs.playlistName,
|
||||||
fragment = this
|
fragment = this
|
||||||
@ -397,7 +407,7 @@ open class TrackCollectionFragment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun triggerButtonUpdate(selection: List<Track> = getSelectedSongs()) {
|
fun triggerButtonUpdate(selection: List<Track> = getSelectedTracks()) {
|
||||||
listModel.calculateButtonState(selection, ::updateButtonState)
|
listModel.calculateButtonState(selection, ::updateButtonState)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,14 +424,14 @@ open class TrackCollectionFragment(
|
|||||||
playNowButton?.isVisible = show.all
|
playNowButton?.isVisible = show.all
|
||||||
playNextButton?.isVisible = show.all && multipleSelection
|
playNextButton?.isVisible = show.all && multipleSelection
|
||||||
playLastButton?.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
|
unpinButton?.isVisible = show.all && show.unpin
|
||||||
downloadButton?.isVisible = show.all && show.download && !isOffline()
|
downloadButton?.isVisible = show.all && show.download && !isOffline()
|
||||||
deleteButton?.isVisible = show.all && show.delete
|
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
|
var songs = tracks
|
||||||
|
|
||||||
if (songs.isEmpty()) {
|
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(
|
downloadHandler.justDownload(
|
||||||
action = DownloadAction.DELETE,
|
action = DownloadAction.DELETE,
|
||||||
fragment = this,
|
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(
|
downloadHandler.justDownload(
|
||||||
action = DownloadAction.UNPIN,
|
action = DownloadAction.UNPIN,
|
||||||
fragment = this,
|
fragment = this,
|
||||||
@ -502,10 +512,7 @@ open class TrackCollectionFragment(
|
|||||||
val playAll = navArgs.autoPlay
|
val playAll = navArgs.autoPlay
|
||||||
|
|
||||||
if (playAll && songCount > 0) {
|
if (playAll && songCount > 0) {
|
||||||
playAll(
|
playAll(navArgs.shuffle, MediaPlayerManager.InsertionMode.CLEAR)
|
||||||
navArgs.shuffle,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
listModel.currentListIsSortable = true
|
listModel.currentListIsSortable = true
|
||||||
@ -513,7 +520,7 @@ open class TrackCollectionFragment(
|
|||||||
Timber.i("Processed list")
|
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.
|
// Walk through selected set and get the Entries based on the saved ids.
|
||||||
return viewAdapter.getCurrentList().mapNotNull {
|
return viewAdapter.getCurrentList().mapNotNull {
|
||||||
if (it is Track && viewAdapter.isSelected(it.longId))
|
if (it is Track && viewAdapter.isSelected(it.longId))
|
||||||
@ -608,20 +615,13 @@ open class TrackCollectionFragment(
|
|||||||
|
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
R.id.song_menu_play_now -> {
|
R.id.song_menu_play_now -> {
|
||||||
playNow(false, songs)
|
playNow(MediaPlayerManager.InsertionMode.CLEAR, songs, true)
|
||||||
}
|
}
|
||||||
R.id.song_menu_play_next -> {
|
R.id.song_menu_play_next -> {
|
||||||
downloadHandler.addTracksToMediaController(
|
playNow(MediaPlayerManager.InsertionMode.AFTER_CURRENT, songs, true)
|
||||||
songs = songs,
|
|
||||||
append = true,
|
|
||||||
playNext = true,
|
|
||||||
autoPlay = false,
|
|
||||||
playlistName = navArgs.playlistName,
|
|
||||||
fragment = this@TrackCollectionFragment
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
R.id.song_menu_play_last -> {
|
R.id.song_menu_play_last -> {
|
||||||
playNow(true, songs)
|
playNow(MediaPlayerManager.InsertionMode.APPEND, songs, true)
|
||||||
}
|
}
|
||||||
R.id.song_menu_pin -> {
|
R.id.song_menu_pin -> {
|
||||||
downloadBackground(true, songs)
|
downloadBackground(true, songs)
|
||||||
|
@ -35,6 +35,7 @@ import org.moire.ultrasonic.R
|
|||||||
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
import org.moire.ultrasonic.api.subsonic.ApiNotSupportedException
|
||||||
import org.moire.ultrasonic.domain.Share
|
import org.moire.ultrasonic.domain.Share
|
||||||
import org.moire.ultrasonic.fragment.FragmentTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle
|
||||||
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.service.OfflineException
|
import org.moire.ultrasonic.service.OfflineException
|
||||||
import org.moire.ultrasonic.subsonic.DownloadAction
|
import org.moire.ultrasonic.subsonic.DownloadAction
|
||||||
@ -165,10 +166,9 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
this,
|
this,
|
||||||
share.id,
|
share.id,
|
||||||
share.name,
|
share.name,
|
||||||
append = false,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
shuffle = false,
|
shuffle = false
|
||||||
playNext = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_play_shuffled -> {
|
R.id.share_menu_play_shuffled -> {
|
||||||
@ -176,10 +176,9 @@ class SharesFragment : Fragment(), KoinComponent {
|
|||||||
this,
|
this,
|
||||||
share.id,
|
share.id,
|
||||||
share.name,
|
share.name,
|
||||||
append = false,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
shuffle = true,
|
shuffle = true,
|
||||||
playNext = false,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_delete -> {
|
R.id.share_menu_delete -> {
|
||||||
|
@ -11,7 +11,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
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.ListenableFuture
|
||||||
import com.google.common.util.concurrent.ListeningExecutorService
|
import com.google.common.util.concurrent.ListeningExecutorService
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
|
@ -7,16 +7,16 @@
|
|||||||
|
|
||||||
package org.moire.ultrasonic.playback
|
package org.moire.ultrasonic.playback
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.media3.common.HeartRating
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
|
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
|
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_MIXED
|
||||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_TITLES
|
import androidx.media3.common.MediaMetadata.MEDIA_TYPE_PLAYLIST
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.common.Rating
|
import androidx.media3.common.Rating
|
||||||
import androidx.media3.common.StarRating
|
import androidx.media3.common.StarRating
|
||||||
import androidx.media3.session.CommandButton
|
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.SearchCriteria
|
||||||
import org.moire.ultrasonic.domain.SearchResult
|
import org.moire.ultrasonic.domain.SearchResult
|
||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.service.RatingManager
|
import org.moire.ultrasonic.service.RatingManager
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
@ -96,11 +95,8 @@ const val PLAY_COMMAND = "play "
|
|||||||
* MediaBrowserService implementation for e.g. Android Auto
|
* MediaBrowserService implementation for e.g. Android Auto
|
||||||
*/
|
*/
|
||||||
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
@Suppress("TooManyFunctions", "LargeClass", "UnusedPrivateMember")
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
class AutoMediaBrowserCallback : MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
||||||
class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|
||||||
MediaLibraryService.MediaLibrarySession.Callback, KoinComponent {
|
|
||||||
|
|
||||||
private val mediaPlayerManager by inject<MediaPlayerManager>()
|
|
||||||
private val activeServerProvider: ActiveServerProvider by inject()
|
private val activeServerProvider: ActiveServerProvider by inject()
|
||||||
|
|
||||||
private val serviceJob = SupervisorJob()
|
private val serviceJob = SupervisorJob()
|
||||||
@ -116,22 +112,59 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
private val isOffline get() = ActiveServerProvider.isOffline()
|
private val isOffline get() = ActiveServerProvider.isOffline()
|
||||||
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
private val musicFolderId get() = activeServerProvider.getActiveServer().musicFolderId
|
||||||
|
|
||||||
private var customCommands: List<CommandButton>
|
private val placeholderButton = getPlaceholderButton()
|
||||||
internal var customLayout = ImmutableList.of<CommandButton>()
|
|
||||||
|
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 {
|
init {
|
||||||
customCommands =
|
val shuffleCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_SHUFFLE, Bundle.EMPTY)
|
||||||
listOf(
|
shuffleButton = getShuffleCommandButton(shuffleCommand)
|
||||||
// This button is used for an unstarred track, and its action will star the track
|
|
||||||
getHeartCommandButton(
|
val repeatCommand = SessionCommand(PlaybackService.CUSTOM_COMMAND_REPEAT_MODE, Bundle.EMPTY)
|
||||||
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON, Bundle.EMPTY)
|
repeatOffButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_OFF)
|
||||||
),
|
repeatOneButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ONE)
|
||||||
// This button is used for an starred track, and its action will unstar the track
|
repeatAllButton = getRepeatModeButton(repeatCommand, Player.REPEAT_MODE_ALL)
|
||||||
getHeartCommandButton(
|
|
||||||
SessionCommand(PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF, Bundle.EMPTY)
|
allCustomCommands = listOf(
|
||||||
)
|
heartButtonToggleOn,
|
||||||
)
|
heartButtonToggleOff,
|
||||||
customLayout = ImmutableList.of(customCommands[0])
|
shuffleButton,
|
||||||
|
repeatOffButton,
|
||||||
|
repeatOneButton,
|
||||||
|
repeatAllButton
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultCustomCommands = listOf(heartButtonToggleOn, shuffleButton, repeatOffButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,8 +208,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
"Root Folder",
|
"Root Folder",
|
||||||
MEDIA_ROOT_ID,
|
MEDIA_ROOT_ID,
|
||||||
isPlayable = false,
|
isPlayable = false,
|
||||||
folderType = FOLDER_TYPE_MIXED,
|
isBrowsable = true,
|
||||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
mediaType = MEDIA_TYPE_FOLDER_MIXED
|
||||||
),
|
),
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
@ -188,14 +221,17 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
controller: MediaSession.ControllerInfo
|
controller: MediaSession.ControllerInfo
|
||||||
): MediaSession.ConnectionResult {
|
): MediaSession.ConnectionResult {
|
||||||
Timber.i("onConnect")
|
Timber.i("onConnect")
|
||||||
|
|
||||||
val connectionResult = super.onConnect(session, controller)
|
val connectionResult = super.onConnect(session, controller)
|
||||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||||
|
|
||||||
for (commandButton in customCommands) {
|
for (commandButton in allCustomCommands) {
|
||||||
// Add custom command to available session commands.
|
// Add custom command to available session commands.
|
||||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.player.repeatMode = Player.REPEAT_MODE_ALL
|
||||||
|
|
||||||
return MediaSession.ConnectionResult.accept(
|
return MediaSession.ConnectionResult.accept(
|
||||||
availableSessionCommands.build(),
|
availableSessionCommands.build(),
|
||||||
connectionResult.availablePlayerCommands
|
connectionResult.availablePlayerCommands
|
||||||
@ -203,26 +239,72 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
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)
|
// Let Media3 controller (for instance the MediaNotificationProvider)
|
||||||
// know about the custom layout right after it connected.
|
// 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 {
|
private fun getHeartCommandButton(sessionCommand: SessionCommand, willHeart: Boolean) =
|
||||||
val willHeart =
|
CommandButton.Builder()
|
||||||
(sessionCommand.customAction == PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON)
|
.setDisplayName(
|
||||||
return CommandButton.Builder()
|
if (willHeart)
|
||||||
.setDisplayName("Love")
|
"Love"
|
||||||
|
else
|
||||||
|
"Dislike"
|
||||||
|
)
|
||||||
.setIconResId(
|
.setIconResId(
|
||||||
if (willHeart) R.drawable.ic_star_hollow
|
if (willHeart)
|
||||||
else R.drawable.ic_star_full
|
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)
|
.setSessionCommand(sessionCommand)
|
||||||
.setEnabled(true)
|
.setEnabled(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetItem(
|
override fun onGetItem(
|
||||||
session: MediaLibraryService.MediaLibrarySession,
|
session: MediaLibraryService.MediaLibrarySession,
|
||||||
@ -266,18 +348,32 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
customCommand: SessionCommand,
|
customCommand: SessionCommand,
|
||||||
args: Bundle
|
args: Bundle
|
||||||
): ListenableFuture<SessionResult> {
|
): ListenableFuture<SessionResult> {
|
||||||
Timber.i("onCustomCommand")
|
Timber.i("onCustomCommand %s", customCommand.customAction)
|
||||||
var customCommandFuture: ListenableFuture<SessionResult>? = null
|
var customCommandFuture: ListenableFuture<SessionResult>? = null
|
||||||
|
|
||||||
when (customCommand.customAction) {
|
when (customCommand.customAction) {
|
||||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> {
|
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_ON -> {
|
||||||
customCommandFuture = onSetRating(session, controller, HeartRating(true))
|
customCommandFuture = onSetRating(session, controller, HeartRating(true))
|
||||||
updateCustomHeartButton(session, true)
|
updateCustomHeartButton(session, isHeart = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> {
|
PlaybackService.CUSTOM_COMMAND_TOGGLE_HEART_OFF -> {
|
||||||
customCommandFuture = onSetRating(session, controller, HeartRating(false))
|
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 -> {
|
else -> {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"CustomCommand not recognized %s with extra %s",
|
"CustomCommand not recognized %s with extra %s",
|
||||||
@ -286,16 +382,23 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (customCommandFuture != null)
|
|
||||||
return customCommandFuture
|
return customCommandFuture
|
||||||
return super.onCustomCommand(session, controller, customCommand, args)
|
?: super.onCustomCommand(
|
||||||
|
session,
|
||||||
|
controller,
|
||||||
|
customCommand,
|
||||||
|
args
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSetRating(
|
override fun onSetRating(
|
||||||
session: MediaSession,
|
session: MediaSession,
|
||||||
controller: MediaSession.ControllerInfo,
|
controller: MediaSession.ControllerInfo,
|
||||||
rating: Rating
|
rating: Rating
|
||||||
): ListenableFuture<SessionResult> {
|
): ListenableFuture<SessionResult> {
|
||||||
val mediaItem = session.player.currentMediaItem
|
val mediaItem = session.player.currentMediaItem
|
||||||
|
|
||||||
if (mediaItem != null) {
|
if (mediaItem != null) {
|
||||||
if (rating is HeartRating) {
|
if (rating is HeartRating) {
|
||||||
mediaItem.toTrack().starred = rating.isHeart
|
mediaItem.toTrack().starred = rating.isHeart
|
||||||
@ -309,6 +412,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
rating
|
rating
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onSetRating(session, controller, rating)
|
return super.onSetRating(session, controller, rating)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -378,6 +482,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
private fun onAddLegacyAutoItems(
|
private fun onAddLegacyAutoItems(
|
||||||
mediaItems: MutableList<MediaItem>
|
mediaItems: MutableList<MediaItem>
|
||||||
): ListenableFuture<List<MediaItem>> {
|
): ListenableFuture<List<MediaItem>> {
|
||||||
|
Timber.i("onAddLegacyAutoItems %s", mediaItems.first().mediaId)
|
||||||
|
|
||||||
val mediaIdParts = mediaItems.first().mediaId.split('|')
|
val mediaIdParts = mediaItems.first().mediaId.split('|')
|
||||||
|
|
||||||
val tracks = when (mediaIdParts.first()) {
|
val tracks = when (mediaIdParts.first()) {
|
||||||
@ -385,10 +491,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
||||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
||||||
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
|
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
|
||||||
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
|
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
|
||||||
@ -400,57 +508,59 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
||||||
mediaIdParts[1], mediaIdParts[2]
|
mediaIdParts[1], mediaIdParts[2]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tracks != null) {
|
return tracks
|
||||||
return Futures.immediateFuture(
|
?.let {
|
||||||
tracks.map { track -> track.toMediaItem() }
|
Futures.immediateFuture(
|
||||||
.toMutableList()
|
it.map { track -> track.toMediaItem() }
|
||||||
)
|
.toMutableList()
|
||||||
}
|
)
|
||||||
|
}
|
||||||
// Fallback to the original list
|
?: Futures.immediateFuture(mediaItems)
|
||||||
return Futures.immediateFuture(mediaItems)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("ReturnCount", "ComplexMethod")
|
@Suppress("ReturnCount", "ComplexMethod")
|
||||||
fun onLoadChildren(
|
private fun onLoadChildren(
|
||||||
parentId: String,
|
parentId: String,
|
||||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||||
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
Timber.d("AutoMediaBrowserService onLoadChildren called. ParentId: %s", parentId)
|
||||||
|
|
||||||
val parentIdParts = parentId.split('|')
|
val parentIdParts = parentId.split('|')
|
||||||
|
|
||||||
when (parentIdParts.first()) {
|
return when (parentIdParts.first()) {
|
||||||
MEDIA_ROOT_ID -> return getRootItems()
|
MEDIA_ROOT_ID -> getRootItems()
|
||||||
MEDIA_LIBRARY_ID -> return getLibrary()
|
MEDIA_LIBRARY_ID -> getLibrary()
|
||||||
MEDIA_ARTIST_ID -> return getArtists()
|
MEDIA_ARTIST_ID -> getArtists()
|
||||||
MEDIA_ARTIST_SECTION -> return getArtists(parentIdParts[1])
|
MEDIA_ARTIST_SECTION -> getArtists(parentIdParts[1])
|
||||||
MEDIA_ALBUM_ID -> return getAlbums(AlbumListType.SORTED_BY_NAME)
|
MEDIA_ALBUM_ID -> getAlbums(AlbumListType.SORTED_BY_NAME)
|
||||||
MEDIA_ALBUM_PAGE_ID -> return getAlbums(
|
MEDIA_ALBUM_PAGE_ID -> getAlbums(
|
||||||
AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
|
AlbumListType.fromName(parentIdParts[1]), parentIdParts[2].toInt()
|
||||||
)
|
)
|
||||||
MEDIA_PLAYLIST_ID -> return getPlaylists()
|
|
||||||
MEDIA_ALBUM_FREQUENT_ID -> return getAlbums(AlbumListType.FREQUENT)
|
MEDIA_PLAYLIST_ID -> getPlaylists()
|
||||||
MEDIA_ALBUM_NEWEST_ID -> return getAlbums(AlbumListType.NEWEST)
|
MEDIA_ALBUM_FREQUENT_ID -> getAlbums(AlbumListType.FREQUENT)
|
||||||
MEDIA_ALBUM_RECENT_ID -> return getAlbums(AlbumListType.RECENT)
|
MEDIA_ALBUM_NEWEST_ID -> getAlbums(AlbumListType.NEWEST)
|
||||||
MEDIA_ALBUM_RANDOM_ID -> return getAlbums(AlbumListType.RANDOM)
|
MEDIA_ALBUM_RECENT_ID -> getAlbums(AlbumListType.RECENT)
|
||||||
MEDIA_ALBUM_STARRED_ID -> return getAlbums(AlbumListType.STARRED)
|
MEDIA_ALBUM_RANDOM_ID -> getAlbums(AlbumListType.RANDOM)
|
||||||
MEDIA_SONG_RANDOM_ID -> return getRandomSongs()
|
MEDIA_ALBUM_STARRED_ID -> getAlbums(AlbumListType.STARRED)
|
||||||
MEDIA_SONG_STARRED_ID -> return getStarredSongs()
|
MEDIA_SONG_RANDOM_ID -> getRandomSongs()
|
||||||
MEDIA_SHARE_ID -> return getShares()
|
MEDIA_SONG_STARRED_ID -> getStarredSongs()
|
||||||
MEDIA_BOOKMARK_ID -> return getBookmarks()
|
MEDIA_SHARE_ID -> getShares()
|
||||||
MEDIA_PODCAST_ID -> return getPodcasts()
|
MEDIA_BOOKMARK_ID -> getBookmarks()
|
||||||
MEDIA_PLAYLIST_ITEM -> return getPlaylist(parentIdParts[1], parentIdParts[2])
|
MEDIA_PODCAST_ID -> getPodcasts()
|
||||||
MEDIA_ARTIST_ITEM -> return getAlbumsForArtist(
|
MEDIA_PLAYLIST_ITEM -> getPlaylist(parentIdParts[1], parentIdParts[2])
|
||||||
|
MEDIA_ARTIST_ITEM -> getAlbumsForArtist(
|
||||||
parentIdParts[1], parentIdParts[2]
|
parentIdParts[1], parentIdParts[2]
|
||||||
)
|
)
|
||||||
MEDIA_ALBUM_ITEM -> return getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
|
||||||
MEDIA_SHARE_ITEM -> return getSongsForShare(parentIdParts[1])
|
MEDIA_ALBUM_ITEM -> getSongsForAlbum(parentIdParts[1], parentIdParts[2])
|
||||||
MEDIA_PODCAST_ITEM -> return getPodcastEpisodes(parentIdParts[1])
|
MEDIA_SHARE_ITEM -> getSongsForShare(parentIdParts[1])
|
||||||
else -> return Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
|
MEDIA_PODCAST_ITEM -> getPodcastEpisodes(parentIdParts[1])
|
||||||
|
else -> Futures.immediateFuture(LibraryResult.ofItemList(listOf(), null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,8 +593,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
album.title ?: "",
|
album.title ?: "",
|
||||||
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
||||||
.joinToString("|"),
|
.joinToString("|")
|
||||||
FOLDER_TYPE_ALBUMS
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,10 +643,12 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
MEDIA_PLAYLIST_SONG_ITEM -> playPlaylistSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
MEDIA_ALBUM_ITEM -> playAlbum(mediaIdParts[1], mediaIdParts[2])
|
||||||
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
MEDIA_ALBUM_SONG_ITEM -> playAlbumSong(
|
||||||
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
mediaIdParts[1], mediaIdParts[2], mediaIdParts[3]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
MEDIA_SONG_STARRED_ID -> playStarredSongs()
|
||||||
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
|
MEDIA_SONG_STARRED_ITEM -> playStarredSong(mediaIdParts[1])
|
||||||
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
|
MEDIA_SONG_RANDOM_ID -> playRandomSongs()
|
||||||
@ -549,6 +660,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
MEDIA_PODCAST_EPISODE_ITEM -> playPodcastEpisode(
|
||||||
mediaIdParts[1], mediaIdParts[2]
|
mediaIdParts[1], mediaIdParts[2]
|
||||||
)
|
)
|
||||||
|
|
||||||
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
MEDIA_SEARCH_SONG_ITEM -> playSearch(mediaIdParts[1])
|
||||||
else -> {
|
else -> {
|
||||||
listOf()
|
listOf()
|
||||||
@ -573,7 +685,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
R.string.music_library_label,
|
R.string.music_library_label,
|
||||||
MEDIA_LIBRARY_ID,
|
MEDIA_LIBRARY_ID,
|
||||||
null,
|
null,
|
||||||
folderType = FOLDER_TYPE_MIXED,
|
isBrowsable = true,
|
||||||
|
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
||||||
icon = R.drawable.ic_library
|
icon = R.drawable.ic_library
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -581,7 +694,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
R.string.main_artists_title,
|
R.string.main_artists_title,
|
||||||
MEDIA_ARTIST_ID,
|
MEDIA_ARTIST_ID,
|
||||||
null,
|
null,
|
||||||
folderType = FOLDER_TYPE_ARTISTS,
|
isBrowsable = true,
|
||||||
|
mediaType = MEDIA_TYPE_FOLDER_ARTISTS,
|
||||||
icon = R.drawable.ic_artist
|
icon = R.drawable.ic_artist
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -590,7 +704,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
MEDIA_ALBUM_ID,
|
MEDIA_ALBUM_ID,
|
||||||
null,
|
null,
|
||||||
folderType = FOLDER_TYPE_ALBUMS,
|
isBrowsable = true,
|
||||||
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||||
icon = R.drawable.ic_menu_browse
|
icon = R.drawable.ic_menu_browse
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -598,7 +713,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
R.string.playlist_label,
|
R.string.playlist_label,
|
||||||
MEDIA_PLAYLIST_ID,
|
MEDIA_PLAYLIST_ID,
|
||||||
null,
|
null,
|
||||||
folderType = FOLDER_TYPE_PLAYLISTS,
|
isBrowsable = true,
|
||||||
|
mediaType = MEDIA_TYPE_FOLDER_PLAYLISTS,
|
||||||
icon = R.drawable.ic_menu_playlists
|
icon = R.drawable.ic_menu_playlists
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -613,14 +729,16 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
R.string.main_songs_random,
|
R.string.main_songs_random,
|
||||||
MEDIA_SONG_RANDOM_ID,
|
MEDIA_SONG_RANDOM_ID,
|
||||||
R.string.main_songs_title,
|
R.string.main_songs_title,
|
||||||
folderType = FOLDER_TYPE_TITLES
|
isBrowsable = true,
|
||||||
|
mediaType = MEDIA_TYPE_PLAYLIST
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_songs_starred,
|
R.string.main_songs_starred,
|
||||||
MEDIA_SONG_STARRED_ID,
|
MEDIA_SONG_STARRED_ID,
|
||||||
R.string.main_songs_title,
|
R.string.main_songs_title,
|
||||||
folderType = FOLDER_TYPE_TITLES
|
isBrowsable = true,
|
||||||
|
mediaType = MEDIA_TYPE_PLAYLIST
|
||||||
)
|
)
|
||||||
|
|
||||||
// Albums
|
// Albums
|
||||||
@ -634,28 +752,28 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
R.string.main_albums_recent,
|
R.string.main_albums_recent,
|
||||||
MEDIA_ALBUM_RECENT_ID,
|
MEDIA_ALBUM_RECENT_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
folderType = FOLDER_TYPE_ALBUMS
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_albums_frequent,
|
R.string.main_albums_frequent,
|
||||||
MEDIA_ALBUM_FREQUENT_ID,
|
MEDIA_ALBUM_FREQUENT_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
folderType = FOLDER_TYPE_ALBUMS
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_albums_random,
|
R.string.main_albums_random,
|
||||||
MEDIA_ALBUM_RANDOM_ID,
|
MEDIA_ALBUM_RANDOM_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
folderType = FOLDER_TYPE_ALBUMS
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||||
)
|
)
|
||||||
|
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
R.string.main_albums_starred,
|
R.string.main_albums_starred,
|
||||||
MEDIA_ALBUM_STARRED_ID,
|
MEDIA_ALBUM_STARRED_ID,
|
||||||
R.string.main_albums_title,
|
R.string.main_albums_title,
|
||||||
folderType = FOLDER_TYPE_ALBUMS
|
mediaType = MEDIA_TYPE_FOLDER_ALBUMS,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
@ -704,8 +822,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
index.add(currentSection)
|
index.add(currentSection)
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
currentSection,
|
currentSection,
|
||||||
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|"),
|
listOf(MEDIA_ARTIST_SECTION, currentSection).joinToString("|")
|
||||||
FOLDER_TYPE_ARTISTS
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -713,8 +830,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
artists.map { artist ->
|
artists.map { artist ->
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
artist.name ?: "",
|
artist.name ?: "",
|
||||||
listOf(childMediaId, artist.id, artist.name).joinToString("|"),
|
listOf(childMediaId, artist.id, artist.name).joinToString("|")
|
||||||
FOLDER_TYPE_ARTISTS
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -744,8 +860,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
album.title ?: "",
|
album.title ?: "",
|
||||||
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
||||||
.joinToString("|"),
|
.joinToString("|")
|
||||||
FOLDER_TYPE_ALBUMS
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
@ -768,15 +883,24 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
mediaItems.addPlayAllItem(listOf(MEDIA_ALBUM_ITEM, id, name).joinToString("|"))
|
||||||
|
|
||||||
// TODO: Paging is not implemented for songs, is it necessary at all?
|
// 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 ->
|
items.map { item ->
|
||||||
if (item.isDirectory)
|
if (item.isDirectory)
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
item.title ?: "",
|
item.title ?: "",
|
||||||
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|"),
|
listOf(MEDIA_ALBUM_ITEM, item.id, item.name).joinToString("|")
|
||||||
FOLDER_TYPE_TITLES
|
|
||||||
)
|
)
|
||||||
else
|
else if (item is Track)
|
||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
item.toMediaItem(
|
item.toMediaItem(
|
||||||
listOf(
|
listOf(
|
||||||
@ -789,6 +913,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -822,8 +947,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
album.title ?: "",
|
album.title ?: "",
|
||||||
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
listOf(MEDIA_ALBUM_ITEM, album.id, album.name)
|
||||||
.joinToString("|"),
|
.joinToString("|")
|
||||||
FOLDER_TYPE_ALBUMS
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -851,7 +975,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
playlist.name,
|
playlist.name,
|
||||||
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
listOf(MEDIA_PLAYLIST_ITEM, playlist.id, playlist.name)
|
||||||
.joinToString("|"),
|
.joinToString("|"),
|
||||||
FOLDER_TYPE_PLAYLISTS
|
mediaType = MEDIA_TYPE_PLAYLIST,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
@ -945,7 +1069,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
mediaItems.add(
|
mediaItems.add(
|
||||||
podcast.title ?: "",
|
podcast.title ?: "",
|
||||||
listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"),
|
listOf(MEDIA_PODCAST_ITEM, podcast.id).joinToString("|"),
|
||||||
FOLDER_TYPE_MIXED
|
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
@ -1048,7 +1172,7 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
share.name ?: "",
|
share.name ?: "",
|
||||||
listOf(MEDIA_SHARE_ITEM, share.id)
|
listOf(MEDIA_SHARE_ITEM, share.id)
|
||||||
.joinToString("|"),
|
.joinToString("|"),
|
||||||
FOLDER_TYPE_MIXED
|
mediaType = MEDIA_TYPE_FOLDER_MIXED,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return@future LibraryResult.ofItemList(mediaItems, null)
|
return@future LibraryResult.ofItemList(mediaItems, null)
|
||||||
@ -1226,14 +1350,16 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
private fun MutableList<MediaItem>.add(
|
private fun MutableList<MediaItem>.add(
|
||||||
title: String,
|
title: String,
|
||||||
mediaId: String,
|
mediaId: String,
|
||||||
folderType: Int
|
mediaType: Int = MEDIA_TYPE_MIXED,
|
||||||
|
isBrowsable: Boolean = false
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val mediaItem = buildMediaItem(
|
val mediaItem = buildMediaItem(
|
||||||
title,
|
title,
|
||||||
mediaId,
|
mediaId,
|
||||||
isPlayable = false,
|
isPlayable = false,
|
||||||
folderType = folderType
|
isBrowsable = isBrowsable,
|
||||||
|
mediaType = mediaType
|
||||||
)
|
)
|
||||||
|
|
||||||
this.add(mediaItem)
|
this.add(mediaItem)
|
||||||
@ -1244,8 +1370,8 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
resId: Int,
|
resId: Int,
|
||||||
mediaId: String,
|
mediaId: String,
|
||||||
groupNameId: Int?,
|
groupNameId: Int?,
|
||||||
browsable: Boolean = true,
|
isBrowsable: Boolean = true,
|
||||||
folderType: Int = FOLDER_TYPE_MIXED,
|
mediaType: Int = MEDIA_TYPE_FOLDER_MIXED,
|
||||||
icon: Int? = null
|
icon: Int? = null
|
||||||
) {
|
) {
|
||||||
val applicationContext = UApp.applicationContext()
|
val applicationContext = UApp.applicationContext()
|
||||||
@ -1253,14 +1379,15 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
val mediaItem = buildMediaItem(
|
val mediaItem = buildMediaItem(
|
||||||
applicationContext.getString(resId),
|
applicationContext.getString(resId),
|
||||||
mediaId,
|
mediaId,
|
||||||
isPlayable = !browsable,
|
isPlayable = !isBrowsable,
|
||||||
folderType = folderType,
|
isBrowsable = isBrowsable,
|
||||||
|
imageUri = if (icon != null) {
|
||||||
|
Util.getUriToDrawable(applicationContext, icon)
|
||||||
|
} else null,
|
||||||
group = if (groupNameId != null) {
|
group = if (groupNameId != null) {
|
||||||
applicationContext.getString(groupNameId)
|
applicationContext.getString(groupNameId)
|
||||||
} else null,
|
} else null,
|
||||||
imageUri = if (icon != null) {
|
mediaType = mediaType
|
||||||
Util.getUriToDrawable(applicationContext, icon)
|
|
||||||
} else null
|
|
||||||
)
|
)
|
||||||
|
|
||||||
this.add(mediaItem)
|
this.add(mediaItem)
|
||||||
@ -1294,14 +1421,102 @@ class AutoMediaBrowserCallback(val libraryService: MediaLibraryService) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCustomHeartButton(
|
private fun Player.setNextRepeatMode() {
|
||||||
session: MediaSession,
|
repeatMode =
|
||||||
isHeart: Boolean
|
when (repeatMode) {
|
||||||
) {
|
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ALL
|
||||||
val command = if (isHeart) customCommands[1] else customCommands[0]
|
Player.REPEAT_MODE_ALL -> Player.REPEAT_MODE_ONE
|
||||||
// Change the custom layout to contain the right heart button
|
else -> Player.REPEAT_MODE_OFF
|
||||||
customLayout = ImmutableList.of(command)
|
}
|
||||||
// Send the updated custom layout to controllers.
|
}
|
||||||
session.setCustomLayout(customLayout)
|
|
||||||
|
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() {
|
private fun initializeSessionAndPlayer() {
|
||||||
if (isStarted) return
|
if (isStarted) return
|
||||||
|
|
||||||
setMediaNotificationProvider(CustomNotificationProvider(UApp.applicationContext()))
|
|
||||||
|
|
||||||
// TODO: Remove minor code duplication with updateBackend()
|
|
||||||
val desiredBackend = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
val desiredBackend = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
||||||
|
Timber.i("Jukebox enabled by default")
|
||||||
MediaPlayerManager.PlayerBackend.JUKEBOX
|
MediaPlayerManager.PlayerBackend.JUKEBOX
|
||||||
} else {
|
} else {
|
||||||
MediaPlayerManager.PlayerBackend.LOCAL
|
MediaPlayerManager.PlayerBackend.LOCAL
|
||||||
}
|
}
|
||||||
|
|
||||||
player = if (activeServerProvider.getActiveServer().jukeboxByDefault) {
|
player = createNewBackend(desiredBackend)
|
||||||
Timber.i("Jukebox enabled by default")
|
|
||||||
getJukeboxPlayer()
|
|
||||||
} else {
|
|
||||||
getLocalPlayer()
|
|
||||||
}
|
|
||||||
|
|
||||||
actualBackend = desiredBackend
|
actualBackend = desiredBackend
|
||||||
|
|
||||||
// Create browser interface
|
// Create browser interface
|
||||||
librarySessionCallback = AutoMediaBrowserCallback(this)
|
librarySessionCallback = AutoMediaBrowserCallback()
|
||||||
|
|
||||||
// This will need to use the AutoCalls
|
// This will need to use the AutoCalls
|
||||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||||
@ -157,10 +150,8 @@ class PlaybackService :
|
|||||||
.setBitmapLoader(ArtworkBitmapLoader())
|
.setBitmapLoader(ArtworkBitmapLoader())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
if (!librarySessionCallback.customLayout.isEmpty()) {
|
// Send custom layout to legacy session.
|
||||||
// Send custom layout to legacy session.
|
mediaLibrarySession.setCustomLayout(librarySessionCallback.defaultCustomCommands)
|
||||||
mediaLibrarySession.setCustomLayout(librarySessionCallback.customLayout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a listener to update the API client when the active server has changed
|
// Set a listener to update the API client when the active server has changed
|
||||||
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
|
||||||
@ -213,11 +204,7 @@ class PlaybackService :
|
|||||||
player.removeListener(listener)
|
player.removeListener(listener)
|
||||||
player.release()
|
player.release()
|
||||||
|
|
||||||
player = if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) {
|
player = createNewBackend(newBackend)
|
||||||
getJukeboxPlayer()
|
|
||||||
} else {
|
|
||||||
getLocalPlayer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add fresh listeners
|
// Add fresh listeners
|
||||||
player.addListener(listener)
|
player.addListener(listener)
|
||||||
@ -227,6 +214,14 @@ class PlaybackService :
|
|||||||
actualBackend = newBackend
|
actualBackend = newBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createNewBackend(newBackend: MediaPlayerManager.PlayerBackend): Player {
|
||||||
|
return if (newBackend == MediaPlayerManager.PlayerBackend.JUKEBOX) {
|
||||||
|
getJukeboxPlayer()
|
||||||
|
} else {
|
||||||
|
getLocalPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getJukeboxPlayer(): Player {
|
private fun getJukeboxPlayer(): Player {
|
||||||
return JukeboxMediaPlayer()
|
return JukeboxMediaPlayer()
|
||||||
}
|
}
|
||||||
@ -425,6 +420,12 @@ class PlaybackService :
|
|||||||
"org.moire.ultrasonic.HEART_ON"
|
"org.moire.ultrasonic.HEART_ON"
|
||||||
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF =
|
const val CUSTOM_COMMAND_TOGGLE_HEART_OFF =
|
||||||
"org.moire.ultrasonic.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
|
private const val NOTIFICATION_ID = 3009
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +82,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
companion object {
|
companion object {
|
||||||
// This is quite important, by setting the DeviceInfo the player is recognized by
|
// This is quite important, by setting the DeviceInfo the player is recognized by
|
||||||
// Android as being a remote playback surface
|
// 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()
|
val running = AtomicBoolean()
|
||||||
const val MAX_GAIN = 10
|
const val MAX_GAIN = 10
|
||||||
}
|
}
|
||||||
@ -206,14 +209,14 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
Player.COMMAND_CHANGE_MEDIA_ITEMS,
|
Player.COMMAND_CHANGE_MEDIA_ITEMS,
|
||||||
Player.COMMAND_GET_TIMELINE,
|
Player.COMMAND_GET_TIMELINE,
|
||||||
Player.COMMAND_GET_DEVICE_VOLUME,
|
Player.COMMAND_GET_DEVICE_VOLUME,
|
||||||
Player.COMMAND_ADJUST_DEVICE_VOLUME,
|
Player.COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
|
||||||
Player.COMMAND_SET_DEVICE_VOLUME
|
Player.COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
|
||||||
)
|
)
|
||||||
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
if (isPlaying) commandsBuilder.add(Player.COMMAND_STOP)
|
||||||
if (playlist.isNotEmpty()) {
|
if (playlist.isNotEmpty()) {
|
||||||
commandsBuilder.addAll(
|
commandsBuilder.addAll(
|
||||||
Player.COMMAND_GET_CURRENT_MEDIA_ITEM,
|
Player.COMMAND_GET_CURRENT_MEDIA_ITEM,
|
||||||
Player.COMMAND_GET_MEDIA_ITEMS_METADATA,
|
Player.COMMAND_GET_METADATA,
|
||||||
Player.COMMAND_PLAY_PAUSE,
|
Player.COMMAND_PLAY_PAUSE,
|
||||||
Player.COMMAND_PREPARE,
|
Player.COMMAND_PREPARE,
|
||||||
Player.COMMAND_SEEK_BACK,
|
Player.COMMAND_SEEK_BACK,
|
||||||
@ -284,6 +287,10 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
override fun setShuffleModeEnabled(shuffleModeEnabled: Boolean) {}
|
||||||
|
|
||||||
override fun setDeviceVolume(volume: Int) {
|
override fun setDeviceVolume(volume: Int) {
|
||||||
|
setDeviceVolume(volume, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDeviceVolume(volume: Int, flags: Int) {
|
||||||
gain = volume
|
gain = volume
|
||||||
tasks.remove(SetGain::class.java)
|
tasks.remove(SetGain::class.java)
|
||||||
tasks.add(SetGain(floatGain))
|
tasks.add(SetGain(floatGain))
|
||||||
@ -299,17 +306,32 @@ class JukeboxMediaPlayer : JukeboxUnimplementedFunctions(), Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun increaseDeviceVolume() {
|
override fun increaseDeviceVolume() {
|
||||||
|
increaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun increaseDeviceVolume(flags: Int) {
|
||||||
gain = (gain + 1).coerceAtMost(MAX_GAIN)
|
gain = (gain + 1).coerceAtMost(MAX_GAIN)
|
||||||
deviceVolume = gain
|
deviceVolume = gain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun decreaseDeviceVolume() {
|
override fun decreaseDeviceVolume() {
|
||||||
|
decreaseDeviceVolume(C.VOLUME_FLAG_SHOW_UI)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decreaseDeviceVolume(flags: Int) {
|
||||||
gain = (gain - 1).coerceAtLeast(0)
|
gain = (gain - 1).coerceAtLeast(0)
|
||||||
deviceVolume = gain
|
deviceVolume = gain
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun setDeviceMuted(muted: Boolean) {
|
override fun setDeviceMuted(muted: Boolean) {
|
||||||
|
setDeviceMuted(muted, C.VOLUME_FLAG_SHOW_UI)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setDeviceMuted(muted: Boolean, flags: Int) {
|
||||||
gain = 0
|
gain = 0
|
||||||
deviceVolume = gain
|
deviceVolume = gain
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,18 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
|||||||
TODO("Not yet implemented")
|
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) {
|
override fun setPlayWhenReady(playWhenReady: Boolean) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
@ -134,11 +146,6 @@ abstract class JukeboxUnimplementedFunctions : Player {
|
|||||||
override fun setPlaybackSpeed(speed: Float) {
|
override fun setPlaybackSpeed(speed: Float) {
|
||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stop(reset: Boolean) {
|
|
||||||
TODO("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentTracks(): Tracks {
|
override fun getCurrentTracks(): Tracks {
|
||||||
// TODO Dummy information is returned for now, this seems to work
|
// TODO Dummy information is returned for now, this seems to work
|
||||||
return Tracks.EMPTY
|
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.
|
// 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
|
// 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.
|
// the right event, and can start playback only then.
|
||||||
if (autoPlay) {
|
if (autoPlay && controller?.isPlaying != true) {
|
||||||
if (isShufflePlayEnabled) {
|
if (isShufflePlayEnabled) {
|
||||||
deferredPlay = {
|
deferredPlay = {
|
||||||
val start = controller?.currentTimeline
|
val start = controller?.currentTimeline
|
||||||
|
@ -68,27 +68,27 @@ class DownloadHandler(
|
|||||||
}
|
}
|
||||||
successString = when (action) {
|
successString = when (action) {
|
||||||
DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString(
|
DownloadAction.DOWNLOAD -> fragment.resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs_downloaded,
|
R.plurals.n_songs_to_be_downloaded,
|
||||||
tracksToDownload.size,
|
tracksToDownload.size,
|
||||||
tracksToDownload.size
|
tracksToDownload.size
|
||||||
)
|
)
|
||||||
DownloadAction.UNPIN -> {
|
DownloadAction.UNPIN -> {
|
||||||
fragment.resources.getQuantityString(
|
fragment.resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs_unpinned,
|
R.plurals.n_songs_unpinned,
|
||||||
tracksToDownload.size,
|
tracksToDownload.size,
|
||||||
tracksToDownload.size
|
tracksToDownload.size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DownloadAction.PIN -> {
|
DownloadAction.PIN -> {
|
||||||
fragment.resources.getQuantityString(
|
fragment.resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs_pinned,
|
R.plurals.n_songs_pinned,
|
||||||
tracksToDownload.size,
|
tracksToDownload.size,
|
||||||
tracksToDownload.size
|
tracksToDownload.size
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DownloadAction.DELETE -> {
|
DownloadAction.DELETE -> {
|
||||||
fragment.resources.getQuantityString(
|
fragment.resources.getQuantityString(
|
||||||
R.plurals.select_album_n_songs_deleted,
|
R.plurals.n_songs_deleted,
|
||||||
tracksToDownload.size,
|
tracksToDownload.size,
|
||||||
tracksToDownload.size
|
tracksToDownload.size
|
||||||
)
|
)
|
||||||
@ -104,10 +104,9 @@ class DownloadHandler(
|
|||||||
name: String? = "",
|
name: String? = "",
|
||||||
isShare: Boolean = false,
|
isShare: Boolean = false,
|
||||||
isDirectory: Boolean = true,
|
isDirectory: Boolean = true,
|
||||||
append: Boolean,
|
insertionMode: MediaPlayerManager.InsertionMode,
|
||||||
autoPlay: Boolean,
|
autoPlay: Boolean,
|
||||||
shuffle: Boolean = false,
|
shuffle: Boolean = false,
|
||||||
playNext: Boolean,
|
|
||||||
isArtist: Boolean = false
|
isArtist: Boolean = false
|
||||||
) {
|
) {
|
||||||
var successString: String? = null
|
var successString: String? = null
|
||||||
@ -119,26 +118,28 @@ class DownloadHandler(
|
|||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
addTracksToMediaController(
|
addTracksToMediaController(
|
||||||
songs = songs,
|
songs = songs,
|
||||||
append = append,
|
insertionMode = insertionMode,
|
||||||
playNext = playNext,
|
|
||||||
autoPlay = autoPlay,
|
autoPlay = autoPlay,
|
||||||
shuffle = shuffle,
|
shuffle = shuffle,
|
||||||
playlistName = null,
|
playlistName = null,
|
||||||
fragment = fragment
|
fragment = fragment
|
||||||
)
|
)
|
||||||
|
|
||||||
// Play Now doesn't get a Toast :)
|
// Play Now doesn't get a Toast :)
|
||||||
if (playNext) {
|
successString = when (insertionMode) {
|
||||||
successString = fragment.resources.getQuantityString(
|
MediaPlayerManager.InsertionMode.AFTER_CURRENT ->
|
||||||
R.plurals.select_album_n_songs_play_next,
|
fragment.resources.getQuantityString(
|
||||||
songs.size,
|
R.plurals.n_songs_added_after_current,
|
||||||
songs.size
|
songs.size,
|
||||||
)
|
songs.size
|
||||||
} else if (append) {
|
)
|
||||||
successString = fragment.resources.getQuantityString(
|
MediaPlayerManager.InsertionMode.APPEND ->
|
||||||
R.plurals.select_album_n_songs_added,
|
fragment.resources.getQuantityString(
|
||||||
songs.size,
|
R.plurals.n_songs_added_to_end,
|
||||||
songs.size
|
songs.size,
|
||||||
)
|
songs.size
|
||||||
|
)
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}) { successString }
|
}) { successString }
|
||||||
@ -146,8 +147,7 @@ class DownloadHandler(
|
|||||||
|
|
||||||
fun addTracksToMediaController(
|
fun addTracksToMediaController(
|
||||||
songs: List<Track>,
|
songs: List<Track>,
|
||||||
append: Boolean,
|
insertionMode: MediaPlayerManager.InsertionMode,
|
||||||
playNext: Boolean,
|
|
||||||
autoPlay: Boolean,
|
autoPlay: Boolean,
|
||||||
shuffle: Boolean = false,
|
shuffle: Boolean = false,
|
||||||
playlistName: String? = null,
|
playlistName: String? = null,
|
||||||
@ -157,12 +157,6 @@ class DownloadHandler(
|
|||||||
|
|
||||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||||
|
|
||||||
val insertionMode = when {
|
|
||||||
append -> MediaPlayerManager.InsertionMode.APPEND
|
|
||||||
playNext -> MediaPlayerManager.InsertionMode.AFTER_CURRENT
|
|
||||||
else -> MediaPlayerManager.InsertionMode.CLEAR
|
|
||||||
}
|
|
||||||
|
|
||||||
if (playlistName != null) {
|
if (playlistName != null) {
|
||||||
mediaPlayerManager.suggestedPlaylistName = playlistName
|
mediaPlayerManager.suggestedPlaylistName = playlistName
|
||||||
}
|
}
|
||||||
@ -173,7 +167,10 @@ class DownloadHandler(
|
|||||||
shuffle,
|
shuffle,
|
||||||
insertionMode
|
insertionMode
|
||||||
)
|
)
|
||||||
if (Settings.shouldTransitionOnPlayback && (!append || autoPlay)) {
|
|
||||||
|
if (Settings.shouldTransitionOnPlayback &&
|
||||||
|
insertionMode == MediaPlayerManager.InsertionMode.CLEAR
|
||||||
|
) {
|
||||||
fragment.findNavController().popBackStack(R.id.playerFragment, true)
|
fragment.findNavController().popBackStack(R.id.playerFragment, true)
|
||||||
fragment.findNavController().navigate(R.id.playerFragment)
|
fragment.findNavController().navigate(R.id.playerFragment)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,8 @@ import androidx.core.net.toUri
|
|||||||
import androidx.media3.common.HeartRating
|
import androidx.media3.common.HeartRating
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
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 androidx.media3.common.StarRating
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
@ -22,7 +23,7 @@ import java.util.Date
|
|||||||
import org.moire.ultrasonic.domain.Track
|
import org.moire.ultrasonic.domain.Track
|
||||||
import org.moire.ultrasonic.provider.AlbumArtContentProvider
|
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
|
// for a single string value
|
||||||
private const val DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE =
|
private const val DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE =
|
||||||
"android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"
|
"android.media.browse.CONTENT_STYLE_GROUP_TITLE_HINT"
|
||||||
@ -76,15 +77,16 @@ fun Track.toMediaItem(
|
|||||||
title = title ?: "",
|
title = title ?: "",
|
||||||
mediaId = mediaId,
|
mediaId = mediaId,
|
||||||
isPlayable = !isDirectory,
|
isPlayable = !isDirectory,
|
||||||
folderType = if (isDirectory) MediaMetadata.FOLDER_TYPE_TITLES
|
isBrowsable = isDirectory,
|
||||||
else MediaMetadata.FOLDER_TYPE_NONE,
|
|
||||||
album = album,
|
album = album,
|
||||||
artist = artist,
|
artist = artist,
|
||||||
genre = genre,
|
genre = genre,
|
||||||
sourceUri = uri.toUri(),
|
sourceUri = uri.toUri(),
|
||||||
imageUri = artworkUri,
|
imageUri = artworkUri,
|
||||||
starred = starred,
|
starred = starred,
|
||||||
group = null
|
group = null,
|
||||||
|
mediaType = if (isDirectory) MEDIA_TYPE_FOLDER_MIXED
|
||||||
|
else MEDIA_TYPE_MUSIC
|
||||||
)
|
)
|
||||||
|
|
||||||
val metadataBuilder = mediaItem.mediaMetadata.buildUpon()
|
val metadataBuilder = mediaItem.mediaMetadata.buildUpon()
|
||||||
@ -204,14 +206,6 @@ private fun safeParseDate(created: String?): Date? {
|
|||||||
} else null
|
} 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.
|
* Build a new MediaItem from a list of attributes.
|
||||||
* Especially useful to create folder entries in the Auto interface.
|
* Especially useful to create folder entries in the Auto interface.
|
||||||
@ -222,7 +216,7 @@ fun buildMediaItem(
|
|||||||
title: String,
|
title: String,
|
||||||
mediaId: String,
|
mediaId: String,
|
||||||
isPlayable: Boolean,
|
isPlayable: Boolean,
|
||||||
folderType: @MediaMetadata.FolderType Int,
|
isBrowsable: Boolean = false,
|
||||||
album: String? = null,
|
album: String? = null,
|
||||||
artist: String? = null,
|
artist: String? = null,
|
||||||
genre: String? = null,
|
genre: String? = null,
|
||||||
@ -241,17 +235,13 @@ fun buildMediaItem(
|
|||||||
.setAlbumArtist(artist)
|
.setAlbumArtist(artist)
|
||||||
.setGenre(genre)
|
.setGenre(genre)
|
||||||
.setUserRating(HeartRating(starred))
|
.setUserRating(HeartRating(starred))
|
||||||
.setFolderType(folderType)
|
.setIsBrowsable(isBrowsable)
|
||||||
.setIsPlayable(isPlayable)
|
.setIsPlayable(isPlayable)
|
||||||
|
|
||||||
if (imageUri != null) {
|
if (imageUri != null) {
|
||||||
metadataBuilder.setArtworkUri(imageUri)
|
metadataBuilder.setArtworkUri(imageUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folderType > FOLDER_TYPE_NONE) {
|
|
||||||
metadataBuilder.setIsBrowsable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaType != null) {
|
if (mediaType != null) {
|
||||||
metadataBuilder.setMediaType(mediaType)
|
metadataBuilder.setMediaType(mediaType)
|
||||||
}
|
}
|
||||||
|
@ -318,7 +318,7 @@
|
|||||||
<string name="server_menu.move_down">Posunout níž</string>
|
<string name="server_menu.move_down">Posunout níž</string>
|
||||||
<string name="server_editor.authentication">Ověření</string>
|
<string name="server_editor.authentication">Ověření</string>
|
||||||
<string name="server_editor.advanced">Rozšířené možnosti</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="one">%d skladba</item>
|
||||||
<item quantity="few">%d skladby</item>
|
<item quantity="few">%d skladby</item>
|
||||||
<item quantity="many">%d skladeb</item>
|
<item quantity="many">%d skladeb</item>
|
||||||
|
@ -383,31 +383,31 @@
|
|||||||
<string name="server_menu.demo">Demo Server</string>
|
<string name="server_menu.demo">Demo Server</string>
|
||||||
<string name="about.webpage">Website besuchen</string>
|
<string name="about.webpage">Website besuchen</string>
|
||||||
<string name="about.report">Einen Fehler melden</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="one">%d Titel</item>
|
||||||
<item quantity="other">%d Titel</item>
|
<item quantity="other">%d Titel</item>
|
||||||
</plurals>
|
</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="one">%d Titel zum Anheften ausgewählt</item>
|
||||||
<item quantity="other">%d Titel zum Anheften ausgewählt</item>
|
<item quantity="other">%d Titel zum Anheften ausgewählt</item>
|
||||||
</plurals>
|
</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="one">%d Titel zum herunterladen ausgewählt</item>
|
||||||
<item quantity="other">%d Titel zum herunterladen ausgewählt</item>
|
<item quantity="other">%d Titel zum herunterladen ausgewählt</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d Titel losgelöst</item>
|
<item quantity="one">%d Titel losgelöst</item>
|
||||||
<item quantity="other">%d Titel losgelöst</item>
|
<item quantity="other">%d Titel losgelöst</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d Titel gelöscht</item>
|
<item quantity="one">%d Titel gelöscht</item>
|
||||||
<item quantity="other">%d Titel gelöscht</item>
|
<item quantity="other">%d Titel gelöscht</item>
|
||||||
</plurals>
|
</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="one">%d Titel am Ende hinzugefügt</item>
|
||||||
<item quantity="other">%d Titel am Ende hinzugefügt</item>
|
<item quantity="other">%d Titel am Ende hinzugefügt</item>
|
||||||
</plurals>
|
</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="one">%d Titel nach aktuellen Titel hinzugefügt</item>
|
||||||
<item quantity="other">%d Titel nach aktuellen Titel hinzugefügt</item>
|
<item quantity="other">%d Titel nach aktuellen Titel hinzugefügt</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
@ -390,37 +390,37 @@
|
|||||||
<string name="about.webpage">Visitar la página web</string>
|
<string name="about.webpage">Visitar la página web</string>
|
||||||
<string name="about.report">Informar de un error</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>
|
<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="one">%d canción</item>
|
||||||
<item quantity="many">%d canciones</item>
|
<item quantity="many">%d canciones</item>
|
||||||
<item quantity="other">%d canciones</item>
|
<item quantity="other">%d canciones</item>
|
||||||
</plurals>
|
</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="one">%d canción seleccionada para ser anclada</item>
|
||||||
<item quantity="many">%d canciones seleccionadas para ser ancladas</item>
|
<item quantity="many">%d canciones seleccionadas para ser ancladas</item>
|
||||||
<item quantity="other">%d canciones seleccionadas para ser ancladas</item>
|
<item quantity="other">%d canciones seleccionadas para ser ancladas</item>
|
||||||
</plurals>
|
</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="one">%d canción seleccionada para ser descargada</item>
|
||||||
<item quantity="many">%d canciones seleccionadas para ser descargadas</item>
|
<item quantity="many">%d canciones seleccionadas para ser descargadas</item>
|
||||||
<item quantity="other">%d canciones seleccionadas para ser descargadas</item>
|
<item quantity="other">%d canciones seleccionadas para ser descargadas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d canción desanclada</item>
|
<item quantity="one">%d canción desanclada</item>
|
||||||
<item quantity="many">%d canciones desancladas</item>
|
<item quantity="many">%d canciones desancladas</item>
|
||||||
<item quantity="other">%d canciones desancladas</item>
|
<item quantity="other">%d canciones desancladas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d canción eliminada</item>
|
<item quantity="one">%d canción eliminada</item>
|
||||||
<item quantity="many">%d canciones eliminadas</item>
|
<item quantity="many">%d canciones eliminadas</item>
|
||||||
<item quantity="other">%d canciones eliminadas</item>
|
<item quantity="other">%d canciones eliminadas</item>
|
||||||
</plurals>
|
</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="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="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>
|
<item quantity="other">%d canciones añadidas al final de la cola de reproducción</item>
|
||||||
</plurals>
|
</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="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="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>
|
<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.webpage">Visiter la page web</string>
|
||||||
<string name="about.report">Signaler un bug</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>
|
<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="one">%d titre</item>
|
||||||
<item quantity="many">%d titres</item>
|
<item quantity="many">%d titres</item>
|
||||||
<item quantity="other">%d titres</item>
|
<item quantity="other">%d titres</item>
|
||||||
@ -400,12 +400,12 @@
|
|||||||
<string name="settings.preload_50">50 morceaux</string>
|
<string name="settings.preload_50">50 morceaux</string>
|
||||||
<string name="chat.user_avatar">Image d\'avatar</string>
|
<string name="chat.user_avatar">Image d\'avatar</string>
|
||||||
<string name="settings.theme_day_night">Jour et nuit</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="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="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>
|
<item quantity="other">%d morceaux ajoutés à la file d\'attente de fin de lecture</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d morceau supprimé</item>
|
<item quantity="one">%d morceau supprimé</item>
|
||||||
<item quantity="many">%d morceaux supprimés</item>
|
<item quantity="many">%d morceaux supprimés</item>
|
||||||
<item quantity="other">%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.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="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>
|
<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="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="many">%d chansons sélectionnées pour téléchargement</item>
|
||||||
<item quantity="other">%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>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d chanson désépinglée</item>
|
<item quantity="one">%d chanson désépinglée</item>
|
||||||
<item quantity="many">%d chansons désépinglées</item>
|
<item quantity="many">%d chansons désépinglées</item>
|
||||||
<item quantity="other">%d chansons désépinglées</item>
|
<item quantity="other">%d chansons désépinglées</item>
|
||||||
</plurals>
|
</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="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="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>
|
<item quantity="other">%d chansons insérées après la chanson en cours</item>
|
||||||
</plurals>
|
</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="one">%d chanson sélectionnée à épingler</item>
|
||||||
<item quantity="many">%d chansons sélectionnées à épingler</item>
|
<item quantity="many">%d chansons sélectionnées à épingler</item>
|
||||||
<item quantity="other">%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.play_last">Reproducir última</string>
|
||||||
<string name="common.pin">Ancorar</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>
|
<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="one">%d canción seleccionada para ser ancorada</item>
|
||||||
<item quantity="other">%d cancións seleccionadas para ser ancoradas</item>
|
<item quantity="other">%d cancións seleccionadas para ser ancoradas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="settings.max_bitrate_pinning">Tasa de bits máxima: ao fixar unha canción de forma permanente</string>
|
<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="one">%d canción desancorada</item>
|
||||||
<item quantity="other">%d cancións desancoradas</item>
|
<item quantity="other">%d cancións desancoradas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
@ -326,7 +326,7 @@
|
|||||||
<string name="server_menu.move_down">Lejjebb mozgat</string>
|
<string name="server_menu.move_down">Lejjebb mozgat</string>
|
||||||
<string name="server_editor.authentication">Bejelentkezés</string>
|
<string name="server_editor.authentication">Bejelentkezés</string>
|
||||||
<string name="server_editor.advanced">Haladó beállítások</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="one">%d dal</item>
|
||||||
<item quantity="other">%d dal</item>
|
<item quantity="other">%d dal</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
@ -302,25 +302,25 @@
|
|||||||
<string name="server_menu.demo">デモサーバー</string>
|
<string name="server_menu.demo">デモサーバー</string>
|
||||||
<string name="about.webpage">Webページにアクセス</string>
|
<string name="about.webpage">Webページにアクセス</string>
|
||||||
<string name="about.report">バグを報告</string>
|
<string name="about.report">バグを報告</string>
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="n_songs">
|
||||||
<item quantity="other">%d 曲</item>
|
<item quantity="other">%d 曲</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_downloaded">
|
<plurals name="n_songs_to_be_downloaded">
|
||||||
<item quantity="other">%d 曲がダウンロード選択されました</item>
|
<item quantity="other">%d 曲がダウンロード選択されました</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="other">%d 曲が固定解除されました</item>
|
<item quantity="other">%d 曲が固定解除されました</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_pinned">
|
<plurals name="n_songs_pinned">
|
||||||
<item quantity="other">%d 曲が固定されるよう選択されました</item>
|
<item quantity="other">%d 曲が固定されるよう選択されました</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="other">%d 曲が削除されました</item>
|
<item quantity="other">%d 曲が削除されました</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_added">
|
<plurals name="n_songs_added_to_end">
|
||||||
<item quantity="other">%d 曲が再生キューの末尾に追加されました</item>
|
<item quantity="other">%d 曲が再生キューの末尾に追加されました</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_play_next">
|
<plurals name="n_songs_added_after_current">
|
||||||
<item quantity="other">%d 曲が現在再生中の曲の次に追加されました</item>
|
<item quantity="other">%d 曲が現在再生中の曲の次に追加されました</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="api.subsonic.generic">一般APIエラー: %1$s</string>
|
<string name="api.subsonic.generic">一般APIエラー: %1$s</string>
|
||||||
|
@ -123,11 +123,11 @@
|
|||||||
<string name="share_set_share_options">Sett delingsinnstillinger</string>
|
<string name="share_set_share_options">Sett delingsinnstillinger</string>
|
||||||
<string name="button_bar.shares">Delinger</string>
|
<string name="button_bar.shares">Delinger</string>
|
||||||
<string name="download.toggle_playlist">Veksle spilleliste</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="one">%d spor lagt til etter nåværende spor</item>
|
||||||
<item quantity="other">%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>
|
||||||
<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="one">%d spor lagt til på slutten av spillekøen</item>
|
||||||
<item quantity="other">%d spor lagt til på slutten av spillekøen</item>
|
<item quantity="other">%d spor lagt til på slutten av spillekøen</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
@ -290,19 +290,19 @@
|
|||||||
<string name="settings.debug.log_to_file">Skriv avlusningslogg til fil</string>
|
<string name="settings.debug.log_to_file">Skriv avlusningslogg til fil</string>
|
||||||
<string name="about.report">Rapporter en feil</string>
|
<string name="about.report">Rapporter en feil</string>
|
||||||
<string name="api.subsonic.generic">Generisk API-feil: %1$s</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="one">%d spor</item>
|
||||||
<item quantity="other">%d spor</item>
|
<item quantity="other">%d spor</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_pinned">
|
<plurals name="n_songs_pinned">
|
||||||
<item quantity="one">%d spor å feste</item>
|
<item quantity="one">%d spor å feste</item>
|
||||||
<item quantity="other">%d spor å feste</item>
|
<item quantity="other">%d spor å feste</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d spor løsnet</item>
|
<item quantity="one">%d spor løsnet</item>
|
||||||
<item quantity="other">%d spor løsnet</item>
|
<item quantity="other">%d spor løsnet</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d spor slettet</item>
|
<item quantity="one">%d spor slettet</item>
|
||||||
<item quantity="other">%d spor løsnet</item>
|
<item quantity="other">%d spor løsnet</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
@ -446,7 +446,7 @@
|
|||||||
<string name="grid_view">Omslag</string>
|
<string name="grid_view">Omslag</string>
|
||||||
<string name="supported_server_features">Støttede funksjoner</string>
|
<string name="supported_server_features">Støttede funksjoner</string>
|
||||||
<string name="jukebox">Jukebox</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="one">%d spor valgt for nedlasting</item>
|
||||||
<item quantity="other">%d spor valgt for nedlasting</item>
|
<item quantity="other">%d spor valgt for nedlasting</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
@ -391,31 +391,31 @@
|
|||||||
<string name="about.webpage">Website openen</string>
|
<string name="about.webpage">Website openen</string>
|
||||||
<string name="about.report">Bug melden</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>
|
<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="one">%d nummer</item>
|
||||||
<item quantity="other">%d nummers</item>
|
<item quantity="other">%d nummers</item>
|
||||||
</plurals>
|
</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="one">%d vast te maken nummer geselecteerd</item>
|
||||||
<item quantity="other">%d vast te maken nummers geselecteerd</item>
|
<item quantity="other">%d vast te maken nummers geselecteerd</item>
|
||||||
</plurals>
|
</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="one">%d te downloaden nummer geselecteerd</item>
|
||||||
<item quantity="other">%d te downloaden nummers geselecteerd</item>
|
<item quantity="other">%d te downloaden nummers geselecteerd</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d nummer losgemaakt</item>
|
<item quantity="one">%d nummer losgemaakt</item>
|
||||||
<item quantity="other">%d nummers losgemaakt</item>
|
<item quantity="other">%d nummers losgemaakt</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d nummer verwijderd</item>
|
<item quantity="one">%d nummer verwijderd</item>
|
||||||
<item quantity="other">%d nummers verwijderd</item>
|
<item quantity="other">%d nummers verwijderd</item>
|
||||||
</plurals>
|
</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="one">%d nummer toegevoegd aan het einde van afspeelwachtrij</item>
|
||||||
<item quantity="other">%d nummers toegevoegd aan het einde van afspeelwachtrij</item>
|
<item quantity="other">%d nummers toegevoegd aan het einde van afspeelwachtrij</item>
|
||||||
</plurals>
|
</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="one">%d nummer ingevoegd na het huidige nummer</item>
|
||||||
<item quantity="other">%d nummers ingevoegd na het huidige nummer</item>
|
<item quantity="other">%d nummers ingevoegd na het huidige nummer</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
@ -307,7 +307,7 @@
|
|||||||
<string name="server_menu.move_down">Przesuń się w dół</string>
|
<string name="server_menu.move_down">Przesuń się w dół</string>
|
||||||
<string name="server_editor.authentication">Authentication</string>
|
<string name="server_editor.authentication">Authentication</string>
|
||||||
<string name="server_editor.advanced">Ustawienia zaawansowane</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="one">%d utwór</item>
|
||||||
<item quantity="few">%d utwory</item>
|
<item quantity="few">%d utwory</item>
|
||||||
<item quantity="many">%d utworów</item>
|
<item quantity="many">%d utworów</item>
|
||||||
@ -356,7 +356,7 @@
|
|||||||
<string name="language.hu">Węgierski</string>
|
<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="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>
|
<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="one">Usunięto %d utwór</item>
|
||||||
<item quantity="few">Usunięto %d utwory</item>
|
<item quantity="few">Usunięto %d utwory</item>
|
||||||
<item quantity="many">Usunięto %d utworów</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.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.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>
|
<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="one">%d utwór zaznaczony do przypięcia</item>
|
||||||
<item quantity="few">%d utwory zaznaczone 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="many">%d utworów zaznaczonych do przypięcia</item>
|
||||||
<item quantity="other">%d utworów zaznaczonych do przypięcia</item>
|
<item quantity="other">%d utworów zaznaczonych do przypięcia</item>
|
||||||
</plurals>
|
</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="one">%d utworów zaznaczonych do pobrania</item>
|
||||||
<item quantity="few">%d utwory zaznaczone do pobrania</item>
|
<item quantity="few">%d utwory zaznaczone do pobrania</item>
|
||||||
<item quantity="many">%d utworów zaznaczonych do pobrania</item>
|
<item quantity="many">%d utworów zaznaczonych do pobrania</item>
|
||||||
<item quantity="other">%d utworów zaznaczonych do pobrania</item>
|
<item quantity="other">%d utworów zaznaczonych do pobrania</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="about.webpage">Odwiedź stronę internetową</string>
|
<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="one">Odpięto %d utwór</item>
|
||||||
<item quantity="few">Odpięto %d utwory</item>
|
<item quantity="few">Odpięto %d utwory</item>
|
||||||
<item quantity="many">Odpięto %d utworów</item>
|
<item quantity="many">Odpięto %d utworów</item>
|
||||||
@ -446,13 +446,13 @@
|
|||||||
<string name="list_view">Lista</string>
|
<string name="list_view">Lista</string>
|
||||||
<string name="grid_view">Okładka</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>
|
<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="one">Dodano %d utwór na koniec kolejki odtwarzania</item>
|
||||||
<item quantity="few">Dodano %d utwory 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="many">Dodano %d utworów na koniec kolejki odtwarzania</item>
|
||||||
<item quantity="other">Dodano %d utworów na koniec kolejki odtwarzania</item>
|
<item quantity="other">Dodano %d utworów na koniec kolejki odtwarzania</item>
|
||||||
</plurals>
|
</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="one">Wstawiono %d utwór po bieżącym utworze</item>
|
||||||
<item quantity="few">Wstawiono %d utwory 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>
|
<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.webpage">Visitar a página web</string>
|
||||||
<string name="about.report">Reportar um erro</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>
|
<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="one">%d música</item>
|
||||||
<item quantity="many">%d músicas</item>
|
<item quantity="many">%d músicas</item>
|
||||||
<item quantity="other">%d músicas</item>
|
<item quantity="other">%d músicas</item>
|
||||||
</plurals>
|
</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="one">%d música selecionada para ser fixada</item>
|
||||||
<item quantity="many">%d músicas selecionadas para serem fixadas</item>
|
<item quantity="many">%d músicas selecionadas para serem fixadas</item>
|
||||||
<item quantity="other">%d músicas selecionadas para serem fixadas</item>
|
<item quantity="other">%d músicas selecionadas para serem fixadas</item>
|
||||||
</plurals>
|
</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="one">%d música selecionada para ser baixada</item>
|
||||||
<item quantity="many">%d músicas selecionadas para serem baixadas</item>
|
<item quantity="many">%d músicas selecionadas para serem baixadas</item>
|
||||||
<item quantity="other">%d músicas selecionadas para serem baixadas</item>
|
<item quantity="other">%d músicas selecionadas para serem baixadas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d música desfixada</item>
|
<item quantity="one">%d música desfixada</item>
|
||||||
<item quantity="many">%d músicas desfixadas</item>
|
<item quantity="many">%d músicas desfixadas</item>
|
||||||
<item quantity="other">%d músicas desfixadas</item>
|
<item quantity="other">%d músicas desfixadas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d música excluída</item>
|
<item quantity="one">%d música excluída</item>
|
||||||
<item quantity="many">%d músicas excluídas</item>
|
<item quantity="many">%d músicas excluídas</item>
|
||||||
<item quantity="other">%d músicas excluídas</item>
|
<item quantity="other">%d músicas excluídas</item>
|
||||||
</plurals>
|
</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="one">%d música adicionada ao final da playlist</item>
|
||||||
<item quantity="many">%d músicas adicionadas 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>
|
<item quantity="other">%d músicas adicionadas ao final da playlist</item>
|
||||||
</plurals>
|
</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="one">%d música adicionada após a atual</item>
|
||||||
<item quantity="many">%d músicas adicionadas 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>
|
<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_menu.move_down">Move down</string>
|
||||||
<string name="server_editor.authentication">Authentication</string>
|
<string name="server_editor.authentication">Authentication</string>
|
||||||
<string name="server_editor.advanced">Configurações avançadas</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="one">%d música</item>
|
||||||
<item quantity="many">%d músicas</item>
|
<item quantity="many">%d músicas</item>
|
||||||
<item quantity="other">%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="server_menu.demo">Servidor Demonstração</string>
|
||||||
<string name="about.webpage">Visitar a página web</string>
|
<string name="about.webpage">Visitar a página web</string>
|
||||||
<string name="about.report">Reportar um erro</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="one">%d música selecionada para ser fixada</item>
|
||||||
<item quantity="many">%d músicas selecionadas para serem fixadas</item>
|
<item quantity="many">%d músicas selecionadas para serem fixadas</item>
|
||||||
<item quantity="other">%d músicas selecionadas para serem fixadas</item>
|
<item quantity="other">%d músicas selecionadas para serem fixadas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d música desfixada</item>
|
<item quantity="one">%d música desfixada</item>
|
||||||
<item quantity="many">%d músicas desfixadas</item>
|
<item quantity="many">%d músicas desfixadas</item>
|
||||||
<item quantity="other">%d músicas desfixadas</item>
|
<item quantity="other">%d músicas desfixadas</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d música excluída</item>
|
<item quantity="one">%d música excluída</item>
|
||||||
<item quantity="many">%d músicas excluídas</item>
|
<item quantity="many">%d músicas excluídas</item>
|
||||||
<item quantity="other">%d músicas excluídas</item>
|
<item quantity="other">%d músicas excluídas</item>
|
||||||
</plurals>
|
</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="one">%d música adicionada ao final da playlist</item>
|
||||||
<item quantity="many">%d músicas adicionadas 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>
|
<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.
|
\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
|
\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>
|
\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="one">%d música selecionada a ser descarregada</item>
|
||||||
<item quantity="many">%d músicas selecionadas para serem baixadas</item>
|
<item quantity="many">%d músicas selecionadas para serem baixadas</item>
|
||||||
<item quantity="other">%d músicas selecionadas para serem baixadas</item>
|
<item quantity="other">%d músicas selecionadas para serem baixadas</item>
|
||||||
</plurals>
|
</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="one">%d música adicionada após a atual</item>
|
||||||
<item quantity="many">%d músicas adicionadas 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>
|
<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_editor.disabled_feature">Одна или несколько функций были отключены, потому что сервер их не поддерживает.\nВы можете запустить этот тест снова в любое время.</string>
|
||||||
<string name="server_menu.demo">Демо-сервер</string>
|
<string name="server_menu.demo">Демо-сервер</string>
|
||||||
|
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="n_songs">
|
||||||
<item quantity="one">%d песня</item>
|
<item quantity="one">%d песня</item>
|
||||||
<item quantity="few">%d песни</item>
|
<item quantity="few">%d песни</item>
|
||||||
<item quantity="many">%d песен</item>
|
<item quantity="many">%d песен</item>
|
||||||
|
@ -368,25 +368,25 @@
|
|||||||
\n通过使用 <b>Ultrasonic</b> 你可以轻松的从你的电脑上的 Subsonic 兼容服务端流式传输或者下载音乐。 Subsonic 服务端与 Ultrasonic 都需要额外的配置才可使用。
|
\n通过使用 <b>Ultrasonic</b> 你可以轻松的从你的电脑上的 Subsonic 兼容服务端流式传输或者下载音乐。 Subsonic 服务端与 Ultrasonic 都需要额外的配置才可使用。
|
||||||
\n
|
\n
|
||||||
\n默认情况下,Ultrasonic 并未进行配置,当服务端配置完成后,请确保配置允许客户端连接到你的计算机。</string>
|
\n默认情况下,Ultrasonic 并未进行配置,当服务端配置完成后,请确保配置允许客户端连接到你的计算机。</string>
|
||||||
<plurals name="select_album_n_songs">
|
<plurals name="n_songs">
|
||||||
<item quantity="other">%d 首曲目</item>
|
<item quantity="other">%d 首曲目</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_pinned">
|
<plurals name="n_songs_pinned">
|
||||||
<item quantity="other">已选择 %d 首歌曲进行固定</item>
|
<item quantity="other">已选择 %d 首歌曲进行固定</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_downloaded">
|
<plurals name="n_songs_to_be_downloaded">
|
||||||
<item quantity="other">已选择要下载 %d 首歌曲</item>
|
<item quantity="other">已选择要下载 %d 首歌曲</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="other">已选择 %d 首歌曲取消固定</item>
|
<item quantity="other">已选择 %d 首歌曲取消固定</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="other">%d 首歌曲被删除</item>
|
<item quantity="other">%d 首歌曲被删除</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_added">
|
<plurals name="n_songs_added_to_end">
|
||||||
<item quantity="other">已将 %d 首歌曲添加到播放队列的末尾</item>
|
<item quantity="other">已将 %d 首歌曲添加到播放队列的末尾</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_play_next">
|
<plurals name="n_songs_added_after_current">
|
||||||
<item quantity="other">在当前歌曲之后插入了 %d 首歌曲</item>
|
<item quantity="other">在当前歌曲之后插入了 %d 首歌曲</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<!-- Subsonic api errors -->
|
<!-- Subsonic api errors -->
|
||||||
|
@ -402,31 +402,35 @@
|
|||||||
<string name="about.webpage.url" translatable="false">https://ultrasonic.gitlab.io/</string>
|
<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>
|
<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="one">%d song</item>
|
||||||
<item quantity="other">%d songs</item>
|
<item quantity="other">%d songs</item>
|
||||||
</plurals>
|
</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="one">%d song selected to be pinned</item>
|
||||||
<item quantity="other">%d songs selected to be pinned</item>
|
<item quantity="other">%d songs selected to be pinned</item>
|
||||||
</plurals>
|
</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="one">%d song selected to be downloaded</item>
|
||||||
<item quantity="other">%d songs selected to be downloaded</item>
|
<item quantity="other">%d songs selected to be downloaded</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_unpinned">
|
<plurals name="n_songs_unpinned">
|
||||||
<item quantity="one">%d song unpinned</item>
|
<item quantity="one">%d song unpinned</item>
|
||||||
<item quantity="other">%d songs unpinned</item>
|
<item quantity="other">%d songs unpinned</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="select_album_n_songs_deleted">
|
<plurals name="n_songs_deleted">
|
||||||
<item quantity="one">%d song deleted</item>
|
<item quantity="one">%d song deleted</item>
|
||||||
<item quantity="other">%d songs deleted</item>
|
<item quantity="other">%d songs deleted</item>
|
||||||
</plurals>
|
</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="one">%d song added to the end of play queue</item>
|
||||||
<item quantity="other">%d songs added to the end of play queue</item>
|
<item quantity="other">%d songs added to the end of play queue</item>
|
||||||
</plurals>
|
</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="one">%d song inserted after current song</item>
|
||||||
<item quantity="other">%d songs inserted after current song</item>
|
<item quantity="other">%d songs inserted after current song</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user