mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-07-23 20:01:56 +03:00
Compare commits
32 Commits
138db03667
...
842cb36ecb
Author | SHA1 | Date | |
---|---|---|---|
|
842cb36ecb | ||
|
e06b8bc22e | ||
|
82fb45bd55 | ||
|
751b946092 | ||
|
39085f68b1 | ||
|
1beb67c497 | ||
|
2ba001894a | ||
|
0650ce0bba | ||
|
218f144848 | ||
|
83c9c188e9 | ||
|
a4e8a7f94d | ||
|
4f5d503ceb | ||
|
381e2e4b86 | ||
|
2a90fe4aab | ||
|
f37301e738 | ||
|
fca5ffaa0c | ||
|
a0314a865c | ||
|
5da9a2819c | ||
|
2a02c94c8f | ||
|
96073125ca | ||
|
58bd663ac0 | ||
|
e689193df1 | ||
|
1aa388d48f | ||
|
8f84020cfa | ||
|
db88ff8431 | ||
|
2d1642170a | ||
|
0cb7952943 | ||
|
d750c84606 | ||
|
7abca537c9 | ||
|
ca2c5483c0 | ||
|
7b414a3a23 | ||
|
10767d2d5b |
@ -13,7 +13,6 @@ dependencies {
|
||||
|
||||
testImplementation libs.kotlinJunit
|
||||
testImplementation libs.mockito
|
||||
testImplementation libs.mockitoInline
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.kluent
|
||||
testImplementation libs.mockWebServer
|
||||
|
8
fastlane/metadata/android/en-US/changelogs/116.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/116.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Bug fixes
|
||||
- Fix various crashes
|
||||
|
||||
Changes since 4.2.0
|
||||
- #827: Make app full compliant Android Auto to publish in Play Store.
|
||||
- #878: "Play shuffled" option for playlists always begins with the first track.
|
||||
- #891: Dump config to log file when logging is enabled.
|
||||
- #854: Remove Videos menu option for servers which don't support it.
|
@ -3,10 +3,10 @@
|
||||
gradle = "7.6"
|
||||
|
||||
navigation = "2.5.3"
|
||||
gradlePlugin = "8.0.0"
|
||||
gradlePlugin = "8.0.1"
|
||||
androidxcore = "1.10.0"
|
||||
ktlint = "0.43.2"
|
||||
ktlintGradle = "11.3.1"
|
||||
ktlintGradle = "11.3.2"
|
||||
detekt = "1.22.0"
|
||||
preferences = "1.2.0"
|
||||
media3 = "1.0.1"
|
||||
@ -16,25 +16,26 @@ materialDesign = "1.8.0"
|
||||
constraintLayout = "2.1.4"
|
||||
multidex = "2.0.1"
|
||||
room = "2.5.1"
|
||||
kotlin = "1.8.20"
|
||||
kotlinxCoroutines = "1.6.4"
|
||||
kotlinxGuava = "1.6.4"
|
||||
kotlin = "1.8.21"
|
||||
kotlinxCoroutines = "1.7.0"
|
||||
kotlinxGuava = "1.7.0"
|
||||
viewModelKtx = "2.6.1"
|
||||
swipeRefresh = "1.1.0"
|
||||
|
||||
retrofit = "2.9.0"
|
||||
jackson = "2.14.2"
|
||||
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
||||
jackson = "2.13.5"
|
||||
okhttp = "4.10.0"
|
||||
koin = "3.3.2"
|
||||
picasso = "2.8"
|
||||
|
||||
junit4 = "4.13.2"
|
||||
junit5 = "5.9.2"
|
||||
mockito = "5.2.0"
|
||||
junit5 = "5.9.3"
|
||||
mockito = "5.3.1"
|
||||
mockitoKotlin = "4.1.0"
|
||||
kluent = "1.72"
|
||||
kluent = "1.73"
|
||||
apacheCodecs = "1.15"
|
||||
robolectric = "4.10"
|
||||
robolectric = "4.10.2"
|
||||
timber = "5.0.1"
|
||||
fastScroll = "2.0.1"
|
||||
colorPicker = "2.2.4"
|
||||
@ -95,7 +96,6 @@ junitVintage = { module = "org.junit.vintage:junit-vintage-engine", v
|
||||
kotlinJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
|
||||
mockitoKotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
|
||||
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
|
||||
mockitoInline = { module = "org.mockito:mockito-inline", version.ref = "mockito" }
|
||||
kluent = { module = "org.amshove.kluent:kluent", version.ref = "kluent" }
|
||||
kluentAndroid = { module = "org.amshove.kluent:kluent-android", version.ref = "kluent" }
|
||||
mockWebServer = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" }
|
||||
|
@ -9,8 +9,8 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId "org.moire.ultrasonic"
|
||||
versionCode 115
|
||||
versionName "4.3.2"
|
||||
versionCode 116
|
||||
versionName "4.3.3"
|
||||
|
||||
minSdkVersion versions.minSdk
|
||||
targetSdkVersion versions.targetSdk
|
||||
@ -137,7 +137,6 @@ dependencies {
|
||||
testImplementation libs.kotlinJunit
|
||||
testImplementation libs.kluent
|
||||
testImplementation libs.mockito
|
||||
testImplementation libs.mockitoInline
|
||||
testImplementation libs.mockitoKotlin
|
||||
testImplementation libs.robolectric
|
||||
|
||||
|
@ -46,7 +46,7 @@ public class GenreAdapter extends ArrayAdapter<Genre> implements SectionIndexer
|
||||
private final Object[] sections;
|
||||
private final Integer[] positions;
|
||||
|
||||
public GenreAdapter(Context context, List<Genre> genres)
|
||||
public GenreAdapter(@NonNull Context context, List<Genre> genres)
|
||||
{
|
||||
super(context, R.layout.list_item_generic, genres);
|
||||
|
||||
|
@ -15,17 +15,17 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.drakeet.multitype.ItemViewDelegate
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.Album
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.subsonic.ImageLoaderProvider
|
||||
import org.moire.ultrasonic.util.LayoutType
|
||||
import org.moire.ultrasonic.util.Settings.shouldUseId3Tags
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Creates a Row in a RecyclerView which contains the details of an Album
|
||||
@ -112,28 +112,14 @@ open class AlbumRowDelegate(
|
||||
private fun onStarClick(entry: Album, star: ImageView) {
|
||||
entry.starred = !entry.starred
|
||||
star.setImageResource(if (entry.starred) starDrawable else starHollowDrawable)
|
||||
val musicService = getMusicService()
|
||||
Thread {
|
||||
val useId3 = shouldUseId3Tags
|
||||
try {
|
||||
if (entry.starred) {
|
||||
musicService.star(
|
||||
if (!useId3) entry.id else null,
|
||||
if (useId3) entry.id else null,
|
||||
null
|
||||
|
||||
RxBus.ratingSubmitter.onNext(
|
||||
RatingUpdate(
|
||||
entry.id,
|
||||
HeartRating(entry.starred)
|
||||
)
|
||||
} else {
|
||||
musicService.unstar(
|
||||
if (!useId3) entry.id else null,
|
||||
if (useId3) entry.id else null,
|
||||
null
|
||||
)
|
||||
}
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(context: Context, parent: ViewGroup): ListViewHolder {
|
||||
return when (layoutType) {
|
||||
|
@ -10,6 +10,7 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
@ -19,10 +20,10 @@ import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.DownloadService
|
||||
import org.moire.ultrasonic.service.DownloadState
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.RxBus
|
||||
import org.moire.ultrasonic.service.plusAssign
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
@ -81,7 +82,6 @@ class TrackViewHolder(val view: View) :
|
||||
draggable: Boolean,
|
||||
isSelected: Boolean = false
|
||||
) {
|
||||
// Timber.v("Setting song")
|
||||
val useFiveStarRating = Settings.useFiveStarRating
|
||||
entry = song
|
||||
|
||||
@ -118,9 +118,9 @@ class TrackViewHolder(val view: View) :
|
||||
}
|
||||
|
||||
if (useFiveStarRating) {
|
||||
setFiveStars(entry?.userRating ?: 0)
|
||||
updateFiveStars(entry?.userRating ?: 0)
|
||||
} else {
|
||||
setSingleStar(entry!!.starred)
|
||||
updateSingleStar(entry!!.starred)
|
||||
}
|
||||
|
||||
if (song.isVideo) {
|
||||
@ -165,48 +165,32 @@ class TrackViewHolder(val view: View) :
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupStarButtons(song: Track, useFiveStarRating: Boolean) {
|
||||
private fun setupStarButtons(track: Track, useFiveStarRating: Boolean) {
|
||||
if (useFiveStarRating) {
|
||||
// Hide single star
|
||||
star.isGone = true
|
||||
rating.isVisible = true
|
||||
val rating = if (song.userRating == null) 0 else song.userRating!!
|
||||
setFiveStars(rating)
|
||||
val rating = if (track.userRating == null) 0 else track.userRating!!
|
||||
updateFiveStars(rating)
|
||||
|
||||
// Five star rating has no click handler because in the
|
||||
// track view theres not enough space
|
||||
} else {
|
||||
star.isVisible = true
|
||||
rating.isGone = true
|
||||
setSingleStar(song.starred)
|
||||
updateSingleStar(track.starred)
|
||||
star.setOnClickListener {
|
||||
val isStarred = song.starred
|
||||
val id = song.id
|
||||
|
||||
if (!isStarred) {
|
||||
star.setImageResource(R.drawable.ic_star_full)
|
||||
song.starred = true
|
||||
} else {
|
||||
star.setImageResource(R.drawable.ic_star_hollow)
|
||||
song.starred = false
|
||||
}
|
||||
|
||||
// Should this be done here ?
|
||||
Thread {
|
||||
val musicService = MusicServiceFactory.getMusicService()
|
||||
try {
|
||||
if (!isStarred) {
|
||||
musicService.star(id, null, null)
|
||||
} else {
|
||||
musicService.unstar(id, null, null)
|
||||
}
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
}.start()
|
||||
track.starred = !track.starred
|
||||
updateSingleStar(track.starred)
|
||||
RxBus.ratingSubmitter.onNext(
|
||||
RatingUpdate(track.id, HeartRating(track.starred))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
private fun setFiveStars(rating: Int) {
|
||||
private fun updateFiveStars(rating: Int) {
|
||||
fiveStar1.setImageResource(
|
||||
if (rating > 0) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
||||
)
|
||||
@ -224,7 +208,7 @@ class TrackViewHolder(val view: View) :
|
||||
)
|
||||
}
|
||||
|
||||
private fun setSingleStar(starred: Boolean) {
|
||||
private fun updateSingleStar(starred: Boolean) {
|
||||
if (starred) {
|
||||
star.setImageResource(R.drawable.ic_star_full)
|
||||
} else {
|
||||
|
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* RatingUpdate.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.data
|
||||
|
||||
import androidx.media3.common.Rating
|
||||
|
||||
data class RatingUpdate(
|
||||
val id: String,
|
||||
val rating: Rating,
|
||||
val success: Boolean? = null
|
||||
)
|
@ -35,11 +35,15 @@ import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import android.widget.ViewFlipper
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.MenuHost
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.SessionResult
|
||||
import androidx.media3.common.StarRating
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
@ -49,8 +53,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
@ -76,6 +78,7 @@ import org.moire.ultrasonic.adapters.TrackViewBinder
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.audiofx.EqualizerController
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.isOffline
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.Identifiable
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
@ -98,7 +101,7 @@ import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Contains the Music Player screen of Ultrasonic with playback controls and the playlist
|
||||
* TODO: Add timeline lister -> updateProgressBar().
|
||||
*
|
||||
*/
|
||||
@Suppress("LargeClass", "TooManyFunctions", "MagicNumber")
|
||||
class PlayerFragment :
|
||||
@ -132,7 +135,6 @@ class PlayerFragment :
|
||||
|
||||
// Views and UI Elements
|
||||
private lateinit var playlistNameView: EditText
|
||||
private lateinit var starMenuItem: MenuItem
|
||||
private lateinit var fiveStar1ImageView: ImageView
|
||||
private lateinit var fiveStar2ImageView: ImageView
|
||||
private lateinit var fiveStar3ImageView: ImageView
|
||||
@ -230,7 +232,13 @@ class PlayerFragment :
|
||||
height = size.y
|
||||
}
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
// Register our options menu
|
||||
(requireActivity() as MenuHost).addMenuProvider(
|
||||
menuProvider,
|
||||
viewLifecycleOwner,
|
||||
Lifecycle.State.RESUMED
|
||||
)
|
||||
|
||||
useFiveStarRating = Settings.useFiveStarRating
|
||||
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100
|
||||
swipeVelocity = swipeDistance
|
||||
@ -467,23 +475,55 @@ class PlayerFragment :
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.nowplaying, menu)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
private val menuProvider: MenuProvider = object : MenuProvider {
|
||||
override fun onPrepareMenu(menu: Menu) {
|
||||
setupOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||
menuInflater.inflate(R.menu.nowplaying, menu)
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
return menuItemSelected(menuItem.itemId, currentSong)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth")
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
fun setupOptionsMenu(menu: Menu) {
|
||||
// Seems there is nothing like ViewBinding for Menus
|
||||
val screenOption = menu.findItem(R.id.menu_item_screen_on_off)
|
||||
val goToAlbum = menu.findItem(R.id.menu_show_album)
|
||||
val goToArtist = menu.findItem(R.id.menu_show_artist)
|
||||
val jukeboxOption = menu.findItem(R.id.menu_item_jukebox)
|
||||
val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer)
|
||||
val shareMenuItem = menu.findItem(R.id.menu_item_share)
|
||||
val shareSongMenuItem = menu.findItem(R.id.menu_item_share_song)
|
||||
starMenuItem = menu.findItem(R.id.menu_item_star)
|
||||
val starMenuItem = menu.findItem(R.id.menu_item_star)
|
||||
val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set)
|
||||
val bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete)
|
||||
|
||||
// Listen to rating changes and update the UI
|
||||
rxBusSubscription += RxBus.ratingPublishedObservable.subscribe { update ->
|
||||
|
||||
// Ignore updates which are not for the current song
|
||||
if (update.id != currentSong?.id) return@subscribe
|
||||
|
||||
// Ensure UI thread
|
||||
launch {
|
||||
if (update.success == true && update.rating is HeartRating) {
|
||||
if (update.rating.isHeart) {
|
||||
starMenuItem.setIcon(fullStar)
|
||||
} else {
|
||||
starMenuItem.setIcon(hollowStar)
|
||||
}
|
||||
} else if (update.success == false) {
|
||||
Toast.makeText(context, "Setting rating failed", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isOffline()) {
|
||||
if (shareMenuItem != null) {
|
||||
shareMenuItem.isVisible = false
|
||||
@ -500,6 +540,7 @@ class PlayerFragment :
|
||||
equalizerMenuItem.isEnabled = isEqualizerAvailable
|
||||
equalizerMenuItem.isVisible = isEqualizerAvailable
|
||||
}
|
||||
|
||||
val mediaPlayerController = mediaPlayerController
|
||||
val track = mediaPlayerController.currentMediaItem?.toTrack()
|
||||
|
||||
@ -512,9 +553,13 @@ class PlayerFragment :
|
||||
if (currentSong != null) {
|
||||
starMenuItem.setIcon(if (currentSong!!.starred) fullStar else hollowStar)
|
||||
shareSongMenuItem.isVisible = true
|
||||
goToAlbum.isVisible = true
|
||||
goToArtist.isVisible = true
|
||||
} else {
|
||||
starMenuItem.setIcon(hollowStar)
|
||||
shareSongMenuItem.isVisible = false
|
||||
goToAlbum.isVisible = false
|
||||
goToArtist.isVisible = false
|
||||
}
|
||||
|
||||
if (mediaPlayerController.keepScreenOn) {
|
||||
@ -555,10 +600,6 @@ class PlayerFragment :
|
||||
return popup
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return menuItemSelected(item.itemId, currentSong) || super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun onContextMenuItemSelected(
|
||||
menuItem: MenuItem,
|
||||
item: MusicDirectory.Child
|
||||
@ -655,31 +696,11 @@ class PlayerFragment :
|
||||
}
|
||||
R.id.menu_item_star -> {
|
||||
if (track == null) return true
|
||||
track.starred = !track.starred
|
||||
|
||||
val isStarred = track.starred
|
||||
|
||||
mediaPlayerController.toggleSongStarred()?.let {
|
||||
Futures.addCallback(
|
||||
it,
|
||||
object : FutureCallback<SessionResult> {
|
||||
override fun onSuccess(result: SessionResult?) {
|
||||
if (isStarred) {
|
||||
starMenuItem.setIcon(hollowStar)
|
||||
track.starred = false
|
||||
} else {
|
||||
starMenuItem.setIcon(fullStar)
|
||||
track.starred = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(t: Throwable) {
|
||||
Toast.makeText(context, "SetRating failed", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
},
|
||||
this.executorService
|
||||
RxBus.ratingSubmitter.onNext(
|
||||
RatingUpdate(track.id, HeartRating(track.starred))
|
||||
)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -1072,8 +1093,6 @@ class PlayerFragment :
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: It would be a lot nicer if MediaPlayerController would send an event
|
||||
// when this is necessary instead of updating every time
|
||||
updateSongRating()
|
||||
|
||||
nextButton.isEnabled = mediaPlayerController.canSeekToNext()
|
||||
@ -1082,7 +1101,6 @@ class PlayerFragment :
|
||||
|
||||
@Synchronized
|
||||
private fun updateSeekBar() {
|
||||
Timber.i("Calling updateSeekBar")
|
||||
val isJukeboxEnabled: Boolean = mediaPlayerController.isJukeboxEnabled
|
||||
val millisPlayed: Int = max(0, mediaPlayerController.playerPosition)
|
||||
val duration: Int = mediaPlayerController.playerDuration
|
||||
@ -1233,11 +1251,7 @@ class PlayerFragment :
|
||||
}
|
||||
|
||||
private fun updateSongRating() {
|
||||
var rating = 0
|
||||
|
||||
if (currentSong?.userRating != null) {
|
||||
rating = currentSong!!.userRating!!
|
||||
}
|
||||
val rating = currentSong?.userRating ?: 0
|
||||
|
||||
fiveStar1ImageView.setImageResource(if (rating > 0) fullStar else hollowStar)
|
||||
fiveStar2ImageView.setImageResource(if (rating > 1) fullStar else hollowStar)
|
||||
@ -1248,8 +1262,15 @@ class PlayerFragment :
|
||||
|
||||
private fun setSongRating(rating: Int) {
|
||||
if (currentSong == null) return
|
||||
currentSong?.userRating = rating
|
||||
updateSongRating()
|
||||
mediaPlayerController.setSongRating(rating)
|
||||
|
||||
RxBus.ratingSubmitter.onNext(
|
||||
RatingUpdate(
|
||||
currentSong!!.id,
|
||||
StarRating(5, rating.toFloat())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
|
@ -401,6 +401,8 @@ open class TrackCollectionFragment(
|
||||
) {
|
||||
// We are coming back from unknown context
|
||||
// and need to ensure Main Thread in order to manipulate the UI
|
||||
// If view is null, our view was disposed in the meantime
|
||||
if (view == null) return
|
||||
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
|
||||
val multipleSelection = viewAdapter.hasMultipleSelection()
|
||||
|
||||
|
@ -102,7 +102,9 @@ class SelectGenreFragment : Fragment() {
|
||||
|
||||
override fun done(result: List<Genre>) {
|
||||
emptyView!!.isVisible = result.isEmpty()
|
||||
genreListView!!.adapter = GenreAdapter(context, result)
|
||||
if (context != null) {
|
||||
genreListView!!.adapter = GenreAdapter(context!!, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
task.execute()
|
||||
|
@ -240,6 +240,8 @@ class ImageLoader(
|
||||
} finally {
|
||||
inputStream.safeClose()
|
||||
}
|
||||
} catch (all: Exception) {
|
||||
Timber.w(all)
|
||||
} finally {
|
||||
cacheInProgress.remove(file)?.countDown()
|
||||
}
|
||||
|
@ -26,8 +26,6 @@ import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import androidx.media3.session.SessionResult.RESULT_ERROR_BAD_VALUE
|
||||
import androidx.media3.session.SessionResult.RESULT_ERROR_UNKNOWN
|
||||
import androidx.media3.session.SessionResult.RESULT_SUCCESS
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
@ -44,12 +42,14 @@ import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.SearchCriteria
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.MediaPlayerController
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||
import org.moire.ultrasonic.service.RatingManager
|
||||
import org.moire.ultrasonic.util.MainThreadExecutor
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
@ -306,22 +306,19 @@ class AutoMediaBrowserCallback(var player: Player, val libraryService: MediaLibr
|
||||
rating: Rating
|
||||
): ListenableFuture<SessionResult> {
|
||||
return serviceScope.future {
|
||||
if (rating is HeartRating) {
|
||||
try {
|
||||
if (rating.isHeart) {
|
||||
musicService.star(mediaId, null, null)
|
||||
} else {
|
||||
musicService.unstar(mediaId, null, null)
|
||||
}
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
// TODO: Better handle exception
|
||||
return@future SessionResult(RESULT_ERROR_UNKNOWN)
|
||||
}
|
||||
Timber.i(controller.packageName)
|
||||
// This function even though its declared in AutoMediaBrowserCallback.kt is
|
||||
// actually called every time we set the rating on an MediaItem.
|
||||
// To avoid an event loop it does not emit a RatingUpdate event,
|
||||
// but calls the Manager directly
|
||||
RatingManager.instance.submitRating(
|
||||
RatingUpdate(
|
||||
id = mediaId,
|
||||
rating = rating
|
||||
)
|
||||
)
|
||||
return@future SessionResult(RESULT_SUCCESS)
|
||||
}
|
||||
return@future SessionResult(RESULT_ERROR_BAD_VALUE)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -10,7 +10,6 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.HeartRating
|
||||
@ -21,28 +20,26 @@ import androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT
|
||||
import androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS
|
||||
import androidx.media3.common.Player.MEDIA_ITEM_TRANSITION_REASON_AUTO
|
||||
import androidx.media3.common.Player.REPEAT_MODE_OFF
|
||||
import androidx.media3.common.Rating
|
||||
import androidx.media3.common.StarRating
|
||||
import androidx.media3.common.Timeline
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionResult
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.common.util.concurrent.FutureCallback
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.app.UApp
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider
|
||||
import org.moire.ultrasonic.data.ActiveServerProvider.Companion.OFFLINE_DB_ID
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.playback.PlaybackService
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import org.moire.ultrasonic.util.MainThreadExecutor
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import org.moire.ultrasonic.util.setPin
|
||||
@ -231,11 +228,21 @@ class MediaPlayerController(
|
||||
clear(false)
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.stopServiceCommandObservable.subscribe {
|
||||
clear(false)
|
||||
onDestroy()
|
||||
}
|
||||
|
||||
rxBusSubscription += RxBus.ratingSubmitterObservable.subscribe {
|
||||
// Ensure correct thread
|
||||
mainScope.launch {
|
||||
// This deals only with the current track!
|
||||
if (it.id != currentMediaItem?.toTrack()?.id) return@launch
|
||||
setRating(it.rating)
|
||||
}
|
||||
}
|
||||
|
||||
created = true
|
||||
Timber.i("MediaPlayerController started")
|
||||
}
|
||||
@ -701,52 +708,49 @@ class MediaPlayerController(
|
||||
controller?.volume = volume
|
||||
}
|
||||
|
||||
fun toggleSongStarred(): ListenableFuture<SessionResult>? {
|
||||
if (currentMediaItem == null) return null
|
||||
val song = currentMediaItem!!.toTrack()
|
||||
|
||||
return (controller as? MediaController)?.setRating(
|
||||
HeartRating(!song.starred)
|
||||
)?.let {
|
||||
Futures.addCallback(
|
||||
it,
|
||||
object : FutureCallback<SessionResult> {
|
||||
override fun onSuccess(result: SessionResult?) {
|
||||
// Trigger an update
|
||||
// TODO Update Metadata of MediaItem...
|
||||
// localMediaPlayer.setCurrentPlaying(localMediaPlayer.currentPlaying)
|
||||
song.starred = !song.starred
|
||||
}
|
||||
|
||||
override fun onFailure(t: Throwable) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"There was an error updating the rating",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
},
|
||||
MainThreadExecutor()
|
||||
)
|
||||
it
|
||||
/*
|
||||
* Sets the rating of the current track
|
||||
*/
|
||||
fun setRating(rating: Rating) {
|
||||
if (controller is MediaController) {
|
||||
(controller as MediaController).setRating(rating)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooGenericExceptionCaught") // The interface throws only generic exceptions
|
||||
fun setSongRating(rating: Int) {
|
||||
if (!Settings.useFiveStarRating) return
|
||||
/*
|
||||
* This legacy function simply emits a rating update,
|
||||
* which will then be processed by both the RatingManager as well as the controller
|
||||
*/
|
||||
fun legacyToggleStar() {
|
||||
if (currentMediaItem == null) return
|
||||
val song = currentMediaItem!!.toTrack()
|
||||
song.userRating = rating
|
||||
mainScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
getMusicService().setRating(song.id, rating)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
val track = currentMediaItem!!.toTrack()
|
||||
track.starred = !track.starred
|
||||
val rating = HeartRating(track.starred)
|
||||
|
||||
RxBus.ratingSubmitter.onNext(
|
||||
RatingUpdate(
|
||||
track.id,
|
||||
rating
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* This legacy function simply emits a rating update,
|
||||
* which will then be processed by both the RatingManager as well as the controller
|
||||
*/
|
||||
fun legacySetRating(num: Int) {
|
||||
if (currentMediaItem == null) return
|
||||
val track = currentMediaItem!!.toTrack()
|
||||
track.userRating = num
|
||||
val rating = StarRating(5, num.toFloat())
|
||||
|
||||
RxBus.ratingSubmitter.onNext(
|
||||
RatingUpdate(
|
||||
track.id,
|
||||
rating
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val currentMediaItem: MediaItem?
|
||||
@ -764,7 +768,6 @@ class MediaPlayerController(
|
||||
* Loops over the timeline windows to find the entry which matches the given closure.
|
||||
*
|
||||
* @param searchClosure Determines the condition which the searched for window needs to match.
|
||||
* @param timeline the timeline to search in.
|
||||
* @return the index of the window that satisfies the search condition,
|
||||
* or [C.INDEX_UNSET] if not found.
|
||||
*/
|
||||
|
@ -30,6 +30,7 @@ import timber.log.Timber
|
||||
* This class is responsible for handling received events for the Media Player implementation
|
||||
*/
|
||||
class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
private lateinit var ratingManager: RatingManager
|
||||
private val playbackStateSerializer by inject<PlaybackStateSerializer>()
|
||||
private val mediaPlayerController by inject<MediaPlayerController>()
|
||||
private val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||
@ -71,6 +72,7 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
|
||||
CacheCleaner().clean()
|
||||
created = true
|
||||
ratingManager = RatingManager.instance
|
||||
Timber.i("LifecycleSupport created")
|
||||
}
|
||||
|
||||
@ -187,12 +189,12 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
||||
KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop()
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> mediaPlayerController.play()
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause()
|
||||
KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1)
|
||||
KeyEvent.KEYCODE_2 -> mediaPlayerController.setSongRating(2)
|
||||
KeyEvent.KEYCODE_3 -> mediaPlayerController.setSongRating(3)
|
||||
KeyEvent.KEYCODE_4 -> mediaPlayerController.setSongRating(4)
|
||||
KeyEvent.KEYCODE_5 -> mediaPlayerController.setSongRating(5)
|
||||
KeyEvent.KEYCODE_STAR -> mediaPlayerController.toggleSongStarred()
|
||||
KeyEvent.KEYCODE_1 -> mediaPlayerController.legacySetRating(1)
|
||||
KeyEvent.KEYCODE_2 -> mediaPlayerController.legacySetRating(2)
|
||||
KeyEvent.KEYCODE_3 -> mediaPlayerController.legacySetRating(3)
|
||||
KeyEvent.KEYCODE_4 -> mediaPlayerController.legacySetRating(4)
|
||||
KeyEvent.KEYCODE_5 -> mediaPlayerController.legacySetRating(5)
|
||||
KeyEvent.KEYCODE_STAR -> mediaPlayerController.legacyToggleStar()
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
@ -39,10 +39,10 @@ interface MusicService {
|
||||
fun getGenres(refresh: Boolean): List<Genre>
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun star(id: String?, albumId: String?, artistId: String?)
|
||||
fun star(id: String?, albumId: String? = null, artistId: String? = null)
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun unstar(id: String?, albumId: String?, artistId: String?)
|
||||
fun unstar(id: String?, albumId: String? = null, artistId: String? = null)
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun setRating(id: String, rating: Int)
|
||||
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* RatingManager.kt
|
||||
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||
*
|
||||
* Distributed under terms of the GNU GPLv3 license.
|
||||
*/
|
||||
|
||||
package org.moire.ultrasonic.service
|
||||
|
||||
import androidx.media3.common.HeartRating
|
||||
import androidx.media3.common.StarRating
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
|
||||
import timber.log.Timber
|
||||
|
||||
/*
|
||||
* This class subscribes to RatingEvents and submits them to the server.
|
||||
* In the future it could be extended to store the ratings when offline
|
||||
* and submit them when back online.
|
||||
* Only the manager should publish RatingSubmitted events
|
||||
*/
|
||||
class RatingManager : CoroutineScope by CoroutineScope(Dispatchers.Default) {
|
||||
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
var lastUpdate: RatingUpdate? = null
|
||||
|
||||
init {
|
||||
rxBusSubscription += RxBus.ratingSubmitterObservable.subscribe {
|
||||
submitRating(it)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun submitRating(update: RatingUpdate) {
|
||||
// Don't submit the same rating twice
|
||||
if (update.id == lastUpdate?.id && update.rating == lastUpdate?.rating) return
|
||||
|
||||
val service = getMusicService()
|
||||
val id = update.id
|
||||
|
||||
Timber.i("Submitting rating to server: ${update.rating} for $id")
|
||||
|
||||
if (update.rating is HeartRating) {
|
||||
launch {
|
||||
var success = false
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
if (update.rating.isHeart) service.star(id)
|
||||
else service.unstar(id)
|
||||
success = true
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
}
|
||||
RxBus.ratingPublished.onNext(
|
||||
update.copy(success = success)
|
||||
)
|
||||
}
|
||||
} else if (update.rating is StarRating) {
|
||||
launch {
|
||||
var success = false
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
getMusicService().setRating(id, update.rating.starRating.toInt())
|
||||
success = true
|
||||
} catch (all: Exception) {
|
||||
Timber.e(all)
|
||||
}
|
||||
}
|
||||
RxBus.ratingPublished.onNext(
|
||||
update.copy(success = success)
|
||||
)
|
||||
}
|
||||
}
|
||||
lastUpdate = update
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: RatingManager by lazy {
|
||||
RatingManager()
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import org.moire.ultrasonic.data.RatingUpdate
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
|
||||
class RxBus {
|
||||
@ -75,6 +76,18 @@ class RxBus {
|
||||
val trackDownloadStateObservable: Observable<TrackDownloadState> =
|
||||
trackDownloadStatePublisher.observeOn(mainThread())
|
||||
|
||||
// Sends a RatingUpdate which was just triggered by the user
|
||||
val ratingSubmitter: PublishSubject<RatingUpdate> =
|
||||
PublishSubject.create()
|
||||
val ratingSubmitterObservable: Observable<RatingUpdate> =
|
||||
ratingSubmitter
|
||||
|
||||
// Sends a RatingUpdate which was successfully submitted to the server or database
|
||||
val ratingPublished: PublishSubject<RatingUpdate> =
|
||||
PublishSubject.create()
|
||||
val ratingPublishedObservable: Observable<RatingUpdate> =
|
||||
ratingPublished
|
||||
|
||||
// Commands
|
||||
val dismissNowPlayingCommandPublisher: PublishSubject<Unit> =
|
||||
PublishSubject.create()
|
||||
|
@ -7,11 +7,14 @@
|
||||
|
||||
package org.moire.ultrasonic.subsonic
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import java.util.Collections
|
||||
import java.util.LinkedList
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@ -28,6 +31,7 @@ import org.moire.ultrasonic.util.EntryByDiscAndTrackComparator
|
||||
import org.moire.ultrasonic.util.InfoDialog
|
||||
import org.moire.ultrasonic.util.Settings
|
||||
import org.moire.ultrasonic.util.Util
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Retrieves a list of songs and adds them to the now playing list
|
||||
@ -39,6 +43,16 @@ class DownloadHandler(
|
||||
) : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
||||
private val maxSongs = 500
|
||||
|
||||
/**
|
||||
* Exception Handler for Coroutines
|
||||
*/
|
||||
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Timber.w(exception)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use coroutine here (with proper exception handler)
|
||||
fun download(
|
||||
fragment: Fragment,
|
||||
append: Boolean,
|
||||
@ -210,7 +224,7 @@ class DownloadHandler(
|
||||
isArtist: Boolean
|
||||
) {
|
||||
// Launch the Job
|
||||
val job = launch {
|
||||
val job = launch(exceptionHandler) {
|
||||
val songs: MutableList<Track> =
|
||||
getTracksFromServer(isArtist, id, isDirectory, name, isShare)
|
||||
|
||||
|
@ -10,7 +10,9 @@ package org.moire.ultrasonic.util
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import java.lang.ref.WeakReference
|
||||
import org.moire.ultrasonic.R
|
||||
import timber.log.Timber
|
||||
|
||||
/*
|
||||
* InfoDialog can be used to show some information to the user. Typically it cannot be cancelled,
|
||||
@ -19,24 +21,30 @@ import org.moire.ultrasonic.R
|
||||
open class InfoDialog(
|
||||
context: Context,
|
||||
message: CharSequence?,
|
||||
private val activity: Activity? = null,
|
||||
activity: Activity? = null,
|
||||
private val finishActivityOnClose: Boolean = false
|
||||
) {
|
||||
|
||||
open var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message)
|
||||
private val activityRef: WeakReference<Activity?> = WeakReference(activity)
|
||||
open var builder: MaterialAlertDialogBuilder = Builder(activityRef.get() ?: context, message)
|
||||
|
||||
fun show() {
|
||||
builder.setOnCancelListener {
|
||||
if (finishActivityOnClose) {
|
||||
activity!!.finish()
|
||||
activityRef.get()?.finish()
|
||||
}
|
||||
}
|
||||
builder.setPositiveButton(R.string.common_ok) { _, _ ->
|
||||
if (finishActivityOnClose) {
|
||||
activity!!.finish()
|
||||
activityRef.get()?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// If the app was put into the background in the meantime this would fail
|
||||
try {
|
||||
builder.create().show()
|
||||
} catch (all: Exception) {
|
||||
Timber.w(all, "Failed to create dialog")
|
||||
}
|
||||
}
|
||||
|
||||
class Builder(context: Context) : MaterialAlertDialogBuilder(context) {
|
||||
@ -93,7 +101,6 @@ class ConfirmationDialog(
|
||||
activity: Activity? = null,
|
||||
finishActivityOnClose: Boolean = false
|
||||
) : InfoDialog(context, message, activity, finishActivityOnClose) {
|
||||
|
||||
override var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message)
|
||||
|
||||
class Builder(context: Context) : MaterialAlertDialogBuilder(context) {
|
||||
|
@ -181,7 +181,7 @@ object Settings {
|
||||
var firstRunExecuted by BooleanSetting(getKey(R.string.setting_key_first_run_executed), false)
|
||||
|
||||
val shouldShowArtistPicture
|
||||
by BooleanSetting(getKey(R.string.setting_key_show_artist_picture), false)
|
||||
by BooleanSetting(getKey(R.string.setting_key_show_artist_picture), true)
|
||||
|
||||
@JvmStatic
|
||||
var chatRefreshInterval by StringIntSetting(
|
||||
|
@ -273,7 +273,7 @@ class StorageFile(
|
||||
}
|
||||
|
||||
private fun getStorageFileForParentDirectory(path: String): StorageFile? {
|
||||
val parentPath = FileUtil.getParentPath(path)!!
|
||||
val parentPath = FileUtil.getParentPath(path) ?: return null
|
||||
if (storageFilePathDictionary.containsKey(parentPath))
|
||||
return storageFilePathDictionary[parentPath]!!
|
||||
if (notExistingPathDictionary.contains(parentPath)) return null
|
||||
|
@ -133,6 +133,8 @@ object Util {
|
||||
@JvmStatic
|
||||
@SuppressLint("ShowToast") // Invalid warning
|
||||
fun toast(context: Context?, message: CharSequence?, shortDuration: Boolean) {
|
||||
// If called after doing some background processing, our context might have expired!
|
||||
try {
|
||||
if (toast == null) {
|
||||
toast = Toast.makeText(
|
||||
context,
|
||||
@ -146,6 +148,9 @@ object Util {
|
||||
if (shortDuration) Toast.LENGTH_SHORT else Toast.LENGTH_LONG
|
||||
}
|
||||
toast!!.show()
|
||||
} catch (_: Exception) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -15,6 +15,13 @@
|
||||
app:showAsAction="ifRoom|withText"
|
||||
a:title="@string/download.menu_star"/>
|
||||
|
||||
<item
|
||||
a:id="@+id/menu_show_artist"
|
||||
a:title="@string/download.menu_show_artist"/>
|
||||
<item
|
||||
a:id="@+id/menu_show_album"
|
||||
a:title="@string/download.menu_show_album"/>
|
||||
|
||||
<item
|
||||
a:id="@+id/menu_item_share_song"
|
||||
a:icon="@drawable/ic_menu_share"
|
||||
|
@ -199,7 +199,7 @@
|
||||
<string name="settings.disc_sort_summary">按光盘编号和曲目编号对歌曲列表进行排序</string>
|
||||
<string name="settings.display_bitrate">展示比特率和文件后缀</string>
|
||||
<string name="settings.display_bitrate_summary">在艺术家姓名后追加比特率和文件后缀</string>
|
||||
<string name="settings.download_transition">正在播放</string>
|
||||
<string name="settings.download_transition">播放时显示正在播放界面</string>
|
||||
<string name="settings.hide_media_summary">隐藏来自其他应用的音乐。</string>
|
||||
<string name="settings.hide_media_title">隐藏其他来源</string>
|
||||
<string name="settings.hide_media_toast">在安卓系统下次扫描音乐时生效。</string>
|
||||
|
@ -213,4 +213,24 @@
|
||||
<string name="music_library.label_offline">離線媒體</string>
|
||||
<string name="playlist.update_info">更新資訊</string>
|
||||
<string name="download.jukebox_not_authorized">不允許遠端控制。請在您在 Subsonic 伺服器上的 <b>使用者 > 設定</b> 中啟用點唱機模式。</string>
|
||||
<string name="settings.directory_cache_time">目錄快取時間</string>
|
||||
<string name="settings.disc_sort_summary">依光碟編號和曲目編號對歌曲清單進行排序</string>
|
||||
<string name="settings.clear_search_history">清空搜尋記錄</string>
|
||||
<string name="settings.chat_refresh">聊天訊息刷新時間間隔</string>
|
||||
<string name="settings.display_bitrate_summary">在藝術家名稱後附加位元速率和檔案後綴</string>
|
||||
<string name="settings.appearance_title">外觀</string>
|
||||
<string name="settings.clear_bookmark_summary">歌曲播放完畢後清除書籤</string>
|
||||
<string name="settings.disc_sort">依光碟排序歌曲</string>
|
||||
<string name="select_album.no_sdcard">錯誤:無可用的 SD 卡。</string>
|
||||
<string name="select_album.no_network">警告:目前沒有可用的網路。
|
||||
\n 如果您要使用行動數據,您需要在設定中允許使用計量付費網路連線下載。</string>
|
||||
<string name="select_album.play_all">播放全部</string>
|
||||
<string name="select_artist.all_folders">所有資料夾</string>
|
||||
<string name="select_artist.folder">選擇資料夾</string>
|
||||
<string name="select_playlist.empty">伺服器上沒有已保存的播放清單</string>
|
||||
<string name="settings.hide_media_title">隱藏其他來源</string>
|
||||
<string name="settings.hide_media_summary">隱藏來自其他應用程式的音樂檔案。</string>
|
||||
<string name="settings.hide_media_toast">在 Android 系統下次掃描裝置內音樂時生效。</string>
|
||||
<string name="settings.download_transition">播放時顯示正在播放介面</string>
|
||||
<string name="settings.download_transition_summary">在媒體庫介面開始播放後切換到正在播放介面</string>
|
||||
</resources>
|
@ -71,7 +71,7 @@
|
||||
<string name="download.menu_save">Save Playlist</string>
|
||||
<string name="download.menu_screen_off">Screen Off</string>
|
||||
<string name="download.menu_screen_on">Screen On</string>
|
||||
<string name="download.menu_show_album">Show Album</string>
|
||||
<string name="download.menu_show_album">Go to Album</string>
|
||||
<string name="download.menu_shuffle">Shuffle</string>
|
||||
<string name="download.menu_shuffle_on">Shuffle mode enabled</string>
|
||||
<string name="download.menu_shuffle_off">Shuffle mode disabled</string>
|
||||
@ -367,7 +367,7 @@
|
||||
<string name="share_default_greeting">Check out this music I shared from %s</string>
|
||||
<string name="share_via">Share songs via</string>
|
||||
<string name="menu.share">Share</string>
|
||||
<string name="download.menu_show_artist">Show Artist</string>
|
||||
<string name="download.menu_show_artist">Go to Artist</string>
|
||||
<string name="albumArt">Album artwork</string>
|
||||
<string name="common_multiple_years">Multiple Years</string>
|
||||
<string name="settings.show_confirmation_dialog">Show confirmation dialog</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user