mirror of
https://gitlab.com/ultrasonic/ultrasonic.git
synced 2025-06-03 09:11:00 +03:00
Compare commits
163 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
1f9de0be7e | ||
|
550c486077 | ||
|
006c554456 | ||
|
6443881193 | ||
|
a98693bbdb | ||
|
03e555eb27 | ||
|
ef73d89491 | ||
|
4ad789d20b | ||
|
7c163c237a | ||
|
38432a3cdc | ||
|
53aee22794 | ||
|
9880f15017 | ||
|
0fc836cf83 | ||
|
6eb20d1c0b | ||
|
1f9c9505d7 | ||
|
2487d24371 | ||
|
c27b3759b1 | ||
|
481f4e0347 | ||
|
a9b47f5aef | ||
|
b53bb38631 | ||
|
a2a42e06b0 | ||
|
adf5b95243 | ||
|
3d82b3935d | ||
|
7ee66e51e8 | ||
|
b126cd594a | ||
|
31f654ee2f | ||
|
42c8edc6e7 | ||
|
8f7c7c33ff | ||
|
c105c0d02d | ||
|
e74fb3795a | ||
|
80923ca8ac | ||
|
f2fa530047 | ||
|
4cb6ab031b | ||
|
2d224e5f84 | ||
|
5a653bebb2 | ||
|
5566cb05ab | ||
|
7ee7aa23be | ||
|
5a2ae50c74 | ||
|
62d7e6bb6d | ||
|
5266fc0b0f | ||
|
612925bc7d | ||
|
81499a1b37 | ||
|
27fc8462d1 | ||
|
f3e205a452 | ||
|
84374f8c13 | ||
|
5e8bf249f8 | ||
|
3913e676b7 | ||
|
a70ee39403 | ||
|
05bd9f0d2c | ||
|
4d9388f8fe | ||
|
8acd7da959 | ||
|
c909848ed5 | ||
|
e5c8e874e3 | ||
|
9d5caed73d | ||
|
292086e9d6 | ||
|
5639fcfb8b | ||
|
ce39da4b79 | ||
|
4e89672d15 | ||
|
6725ef5a2c | ||
|
680cc90871 | ||
|
c19dc125a9 | ||
|
be8fa3c0d1 | ||
|
312a97d664 | ||
|
fca954102a | ||
|
9e078b4879 | ||
|
46e85c27a2 | ||
|
1124cef382 | ||
|
01ebf6e1aa | ||
|
b65050ae85 | ||
|
ddec1d7bdc | ||
|
888d34aeb9 | ||
|
0fb3d8aeca | ||
|
c56913733f | ||
|
76c61e1866 | ||
|
810bb9ddd1 | ||
|
68a2123293 | ||
|
4221b18e09 | ||
|
e7ed066eed | ||
|
1301f7e116 | ||
|
63b064b021 | ||
|
a9be77aa75 | ||
|
fc7dcc9f77 | ||
|
48461f82a8 | ||
|
81ea86fd01 | ||
|
8803f4444e | ||
|
38a97b2b91 | ||
|
6e7bcc4362 | ||
|
c12224e811 | ||
|
b8ef3cd177 | ||
|
2de773e5de | ||
|
fe5b63ad1f | ||
|
4aff5857fd | ||
|
a0d26cb3e7 | ||
|
639ef03bce | ||
|
b855e4bbe7 | ||
|
b83e349f5c | ||
|
abad0438e3 | ||
|
30f02c7eac | ||
|
64f1c3e172 | ||
|
f7f1f40668 | ||
|
6519945c7b | ||
|
94979aeaab | ||
|
37e43b73a7 | ||
|
2cf2cf31c4 | ||
|
9736ae451a | ||
|
26331c1a07 | ||
|
35ffe9ef10 | ||
|
976400d0e1 | ||
|
d2ef76a2c5 | ||
|
c0926b1e13 | ||
|
bf5d41ab30 | ||
|
e2716a5965 | ||
|
8351f1dc0a | ||
|
90997d5f4c | ||
|
747f071f2f | ||
|
9e76308cf2 | ||
|
c452030fe1 | ||
|
dbbeac6084 | ||
|
d56ec198a1 | ||
|
13238dfdc0 | ||
|
8063814bdc | ||
|
3d8abdc65b | ||
|
4832876e54 | ||
|
a0b0409930 | ||
|
8e6e9d4e8e | ||
|
ee6d03db35 | ||
|
364270d338 | ||
|
a4dc06fa8a | ||
|
5c94d995d4 | ||
|
366da1c30c | ||
|
e893510e79 | ||
|
67b359999e | ||
|
01569647f7 | ||
|
3ee20113ae | ||
|
6edff7e053 | ||
|
70cc124818 | ||
|
98bf943a86 | ||
|
58944bb0fd | ||
|
397e1b6ecc | ||
|
71336b3c9f | ||
|
cd47bcf082 | ||
|
4fb5d2a437 | ||
|
e8bf5a38b7 | ||
|
ddfaf520e5 | ||
|
4d1c7464b9 | ||
|
42c6eac97f | ||
|
2ae0a27588 | ||
|
f2e8c0c331 | ||
|
7ce522fd15 | ||
|
170c61ef84 | ||
|
87fdf94e61 | ||
|
fffe245df5 | ||
|
95194bed3e | ||
|
a9467e2fd8 | ||
|
4b99fdb788 | ||
|
727e53e096 | ||
|
c6d26cdd67 | ||
|
458fe5c36e | ||
|
f388aaf4d8 | ||
|
6ddff58afb | ||
|
1e176f995a | ||
|
22c61258cc | ||
|
288a1ad1c2 |
2
.editorconfig
Normal file
2
.editorconfig
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[*.{kt,kts}]
|
||||||
|
ktlint_code_style = android_studio
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,6 +18,7 @@ out/
|
|||||||
|
|
||||||
# Gradle files
|
# Gradle files
|
||||||
.gradle/
|
.gradle/
|
||||||
|
.kotlin/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
default:
|
default:
|
||||||
image: registry.gitlab.com/ultrasonic/ci-android:1.1.0
|
image: registry.gitlab.com/ultrasonic/ci-android:1.2.0
|
||||||
cache: &global_cache
|
cache: &global_cache
|
||||||
key:
|
key:
|
||||||
files:
|
files:
|
||||||
|
2
.idea/compiler.xml
generated
2
.idea/compiler.xml
generated
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="CompilerConfiguration">
|
<component name="CompilerConfiguration">
|
||||||
<bytecodeTargetLevel target="17" />
|
<bytecodeTargetLevel target="21" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -13,7 +13,7 @@ buildscript {
|
|||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
maven { url "https://plugins.gradle.org/m2/" }
|
maven { url = "https://plugins.gradle.org/m2/" }
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath libs.gradle
|
classpath libs.gradle
|
||||||
@ -43,7 +43,7 @@ allprojects {
|
|||||||
// Set Kotlin JVM target to the same for all subprojects
|
// Set Kotlin JVM target to the same for all subprojects
|
||||||
tasks.withType(KotlinCompile).configureEach {
|
tasks.withType(KotlinCompile).configureEach {
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +55,6 @@ allprojects {
|
|||||||
}
|
}
|
||||||
|
|
||||||
wrapper {
|
wrapper {
|
||||||
gradleVersion(libs.versions.gradle.get())
|
gradleVersion = libs.versions.gradle.get()
|
||||||
distributionType("all")
|
distributionType = "all"
|
||||||
}
|
}
|
@ -12,5 +12,9 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'org.moire.ultrasonic.subsonic.domain'
|
namespace = 'org.moire.ultrasonic.subsonic.domain'
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ data class Album(
|
|||||||
override var genre: String? = null,
|
override var genre: String? = null,
|
||||||
override var starred: Boolean = false,
|
override var starred: Boolean = false,
|
||||||
override var path: String? = null,
|
override var path: String? = null,
|
||||||
override var closeness: Int = 0,
|
override var closeness: Int = 0
|
||||||
) : MusicDirectory.Child() {
|
) : MusicDirectory.Child() {
|
||||||
override var isDirectory = true
|
override var isDirectory = true
|
||||||
override var isVideo = false
|
override var isVideo = false
|
||||||
|
@ -13,10 +13,7 @@ class MusicDirectory : ArrayList<MusicDirectory.Child>() {
|
|||||||
var name: String? = null
|
var name: String? = null
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun getChildren(
|
fun getChildren(includeDirs: Boolean = true, includeFiles: Boolean = true): List<Child> {
|
||||||
includeDirs: Boolean = true,
|
|
||||||
includeFiles: Boolean = true
|
|
||||||
): List<Child> {
|
|
||||||
if (includeDirs && includeFiles) {
|
if (includeDirs && includeFiles) {
|
||||||
return toList()
|
return toList()
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,11 @@ plugins {
|
|||||||
|
|
||||||
apply from: bootstrap.kotlinModule
|
apply from: bootstrap.kotlinModule
|
||||||
|
|
||||||
|
java {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_21
|
||||||
|
targetCompatibility = JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
api libs.retrofit
|
api libs.retrofit
|
||||||
api libs.jacksonConverter
|
api libs.jacksonConverter
|
||||||
|
@ -8,7 +8,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
|||||||
* Base class for integration tests for [SubsonicAPIClient] class.
|
* Base class for integration tests for [SubsonicAPIClient] class.
|
||||||
*/
|
*/
|
||||||
abstract class SubsonicAPIClientTest {
|
abstract class SubsonicAPIClientTest {
|
||||||
@JvmField @Rule val mockWebServerRule = MockWebServerRule()
|
@JvmField @Rule
|
||||||
|
val mockWebServerRule = MockWebServerRule()
|
||||||
|
|
||||||
protected lateinit var config: SubsonicClientConfiguration
|
protected lateinit var config: SubsonicClientConfiguration
|
||||||
protected lateinit var client: SubsonicAPIClient
|
protected lateinit var client: SubsonicAPIClient
|
||||||
|
@ -11,7 +11,8 @@ import org.moire.ultrasonic.api.subsonic.rules.MockWebServerRule
|
|||||||
* Base class for testing [okhttp3.Interceptor] implementations.
|
* Base class for testing [okhttp3.Interceptor] implementations.
|
||||||
*/
|
*/
|
||||||
abstract class BaseInterceptorTest {
|
abstract class BaseInterceptorTest {
|
||||||
@Rule @JvmField val mockWebServerRule = MockWebServerRule()
|
@Rule @JvmField
|
||||||
|
val mockWebServerRule = MockWebServerRule()
|
||||||
|
|
||||||
lateinit var client: OkHttpClient
|
lateinit var client: OkHttpClient
|
||||||
|
|
||||||
|
@ -92,7 +92,13 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkVersion(V1_4_0)
|
checkVersion(V1_4_0)
|
||||||
checkParamVersion(musicFolderId, V1_12_0)
|
checkParamVersion(musicFolderId, V1_12_0)
|
||||||
return api.search2(
|
return api.search2(
|
||||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
query,
|
||||||
|
artistCount,
|
||||||
|
artistOffset,
|
||||||
|
albumCount,
|
||||||
|
albumOffset,
|
||||||
|
songCount,
|
||||||
|
musicFolderId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +114,13 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkVersion(V1_8_0)
|
checkVersion(V1_8_0)
|
||||||
checkParamVersion(musicFolderId, V1_12_0)
|
checkParamVersion(musicFolderId, V1_12_0)
|
||||||
return api.search3(
|
return api.search3(
|
||||||
query, artistCount, artistOffset, albumCount, albumOffset, songCount, musicFolderId
|
query,
|
||||||
|
artistCount,
|
||||||
|
artistOffset,
|
||||||
|
albumCount,
|
||||||
|
albumOffset,
|
||||||
|
songCount,
|
||||||
|
musicFolderId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -228,7 +240,13 @@ internal class ApiVersionCheckWrapper(
|
|||||||
checkParamVersion(estimateContentLength, V1_8_0)
|
checkParamVersion(estimateContentLength, V1_8_0)
|
||||||
checkParamVersion(converted, V1_14_0)
|
checkParamVersion(converted, V1_14_0)
|
||||||
return api.stream(
|
return api.stream(
|
||||||
id, maxBitRate, format, timeOffset, videoSize, estimateContentLength, converted
|
id,
|
||||||
|
maxBitRate,
|
||||||
|
format,
|
||||||
|
timeOffset,
|
||||||
|
videoSize,
|
||||||
|
estimateContentLength,
|
||||||
|
converted
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,8 +353,9 @@ internal class ApiVersionCheckWrapper(
|
|||||||
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
private fun checkVersion(expectedVersion: SubsonicAPIVersions) {
|
||||||
// If it is true, it is probably the first call with this server
|
// If it is true, it is probably the first call with this server
|
||||||
if (!isRealProtocolVersion) return
|
if (!isRealProtocolVersion) return
|
||||||
if (currentApiVersion < expectedVersion)
|
if (currentApiVersion < expectedVersion) {
|
||||||
throw ApiNotSupportedException(currentApiVersion)
|
throw ApiNotSupportedException(currentApiVersion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
private fun checkParamVersion(param: Any?, expectedVersion: SubsonicAPIVersions) {
|
||||||
|
@ -90,10 +90,7 @@ interface SubsonicAPIDefinition {
|
|||||||
): Call<SubsonicResponse>
|
): Call<SubsonicResponse>
|
||||||
|
|
||||||
@GET("setRating.view")
|
@GET("setRating.view")
|
||||||
fun setRating(
|
fun setRating(@Query("id") id: String, @Query("rating") rating: Int): Call<SubsonicResponse>
|
||||||
@Query("id") id: String,
|
|
||||||
@Query("rating") rating: Int
|
|
||||||
): Call<SubsonicResponse>
|
|
||||||
|
|
||||||
@GET("getArtist.view")
|
@GET("getArtist.view")
|
||||||
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
fun getArtist(@Query("id") id: String): Call<GetArtistResponse>
|
||||||
@ -158,8 +155,7 @@ interface SubsonicAPIDefinition {
|
|||||||
@Query("public") public: Boolean? = null,
|
@Query("public") public: Boolean? = null,
|
||||||
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
@Query("songIdToAdd") songIdsToAdd: List<String>? = null,
|
||||||
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
@Query("songIndexToRemove") songIndexesToRemove: List<Int>? = null
|
||||||
):
|
): Call<SubsonicResponse>
|
||||||
Call<SubsonicResponse>
|
|
||||||
|
|
||||||
@GET("getPodcasts.view")
|
@GET("getPodcasts.view")
|
||||||
fun getPodcasts(
|
fun getPodcasts(
|
||||||
@ -227,10 +223,7 @@ interface SubsonicAPIDefinition {
|
|||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET("getCoverArt.view")
|
@GET("getCoverArt.view")
|
||||||
fun getCoverArt(
|
fun getCoverArt(@Query("id") id: String, @Query("size") size: Long? = null): Call<ResponseBody>
|
||||||
@Query("id") id: String,
|
|
||||||
@Query("size") size: Long? = null
|
|
||||||
): Call<ResponseBody>
|
|
||||||
|
|
||||||
@Streaming
|
@Streaming
|
||||||
@GET("stream.view")
|
@GET("stream.view")
|
||||||
|
@ -29,10 +29,12 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
|||||||
V1_13_0("5.3", "1.13.0"),
|
V1_13_0("5.3", "1.13.0"),
|
||||||
V1_14_0("6.0", "1.14.0"),
|
V1_14_0("6.0", "1.14.0"),
|
||||||
V1_15_0("6.1", "1.15.0"),
|
V1_15_0("6.1", "1.15.0"),
|
||||||
V1_16_0("6.1.2", "1.16.0");
|
V1_16_0("6.1.2", "1.16.0")
|
||||||
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@JvmStatic @Throws(IllegalArgumentException::class)
|
@JvmStatic
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
fun getClosestKnownClientApiVersion(apiVersion: String): SubsonicAPIVersions {
|
||||||
val versionComponents = apiVersion.split(".")
|
val versionComponents = apiVersion.split(".")
|
||||||
|
|
||||||
@ -41,8 +43,11 @@ enum class SubsonicAPIVersions(val subsonicVersions: String, val restApiVersion:
|
|||||||
try {
|
try {
|
||||||
val majorVersion = versionComponents[0].toInt()
|
val majorVersion = versionComponents[0].toInt()
|
||||||
val minorVersion = versionComponents[1].toInt()
|
val minorVersion = versionComponents[1].toInt()
|
||||||
val patchVersion = if (versionComponents.size > 2) versionComponents[2].toInt()
|
val patchVersion = if (versionComponents.size > 2) {
|
||||||
else 0
|
versionComponents[2].toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
when (majorVersion) {
|
when (majorVersion) {
|
||||||
1 -> when {
|
1 -> when {
|
||||||
|
@ -48,7 +48,10 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
retrofit: Retrofit
|
retrofit: Retrofit
|
||||||
): Converter<*, RequestBody>? {
|
): Converter<*, RequestBody>? {
|
||||||
return jacksonConverterFactory?.requestBodyConverter(
|
return jacksonConverterFactory?.requestBodyConverter(
|
||||||
type, parameterAnnotations, methodAnnotations, retrofit
|
type,
|
||||||
|
parameterAnnotations,
|
||||||
|
methodAnnotations,
|
||||||
|
retrofit
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +66,7 @@ class VersionAwareJacksonConverterFactory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class VersionAwareResponseBodyConverter<T> (
|
class VersionAwareResponseBodyConverter<T>(
|
||||||
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
private val notifier: (SubsonicAPIVersions) -> Unit = {},
|
||||||
private val adapter: ObjectReader
|
private val adapter: ObjectReader
|
||||||
) : Converter<ResponseBody, T> {
|
) : Converter<ResponseBody, T> {
|
||||||
|
@ -6,6 +6,7 @@ import okhttp3.Interceptor.Chain
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
|
||||||
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
internal const val SOCKET_READ_TIMEOUT_DOWNLOAD = 30 * 1000
|
||||||
|
|
||||||
// Allow 20 seconds extra timeout pear MB offset.
|
// Allow 20 seconds extra timeout pear MB offset.
|
||||||
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
internal const val TIMEOUT_MILLIS_PER_OFFSET_BYTE = 0.02
|
||||||
|
|
||||||
|
@ -23,7 +23,8 @@ enum class AlbumListType(val typeName: String) {
|
|||||||
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
SORTED_BY_ARTIST("alphabeticalByArtist"),
|
||||||
STARRED("starred"),
|
STARRED("starred"),
|
||||||
BY_YEAR("byYear"),
|
BY_YEAR("byYear"),
|
||||||
BY_GENRE("byGenre");
|
BY_GENRE("byGenre")
|
||||||
|
;
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return typeName
|
return typeName
|
||||||
|
@ -16,7 +16,8 @@ enum class JukeboxAction(val action: String) {
|
|||||||
CLEAR("clear"),
|
CLEAR("clear"),
|
||||||
REMOVE("remove"),
|
REMOVE("remove"),
|
||||||
SHUFFLE("shuffle"),
|
SHUFFLE("shuffle"),
|
||||||
SET_GAIN("setGain");
|
SET_GAIN("setGain")
|
||||||
|
;
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return action
|
return action
|
||||||
|
@ -10,7 +10,8 @@ class BookmarksResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("bookmarks") private val bookmarksWrapper = BookmarkWrapper()
|
@JsonProperty("bookmarks")
|
||||||
|
private val bookmarksWrapper = BookmarkWrapper()
|
||||||
|
|
||||||
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
val bookmarkList: List<Bookmark> get() = bookmarksWrapper.bookmarkList
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class ChatMessagesResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("chatMessages") private val wrapper = ChatMessagesWrapper()
|
@JsonProperty("chatMessages")
|
||||||
|
private val wrapper = ChatMessagesWrapper()
|
||||||
|
|
||||||
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
val chatMessages: List<ChatMessage> get() = wrapper.messagesList
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class GenresResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("genres") private val genresWrapper = GenresWrapper()
|
@JsonProperty("genres")
|
||||||
|
private val genresWrapper = GenresWrapper()
|
||||||
val genresList: List<Genre> get() = genresWrapper.genresList
|
val genresList: List<Genre> get() = genresWrapper.genresList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ class GetAlbumList2Response(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("albumList2") private val albumWrapper2 = AlbumWrapper2()
|
@JsonProperty("albumList2")
|
||||||
|
private val albumWrapper2 = AlbumWrapper2()
|
||||||
|
|
||||||
val albumList: List<Album>
|
val albumList: List<Album>
|
||||||
get() = albumWrapper2.albumList
|
get() = albumWrapper2.albumList
|
||||||
|
@ -10,7 +10,8 @@ class GetAlbumListResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("albumList") private val albumWrapper = AlbumWrapper()
|
@JsonProperty("albumList")
|
||||||
|
private val albumWrapper = AlbumWrapper()
|
||||||
|
|
||||||
val albumList: List<Album>
|
val albumList: List<Album>
|
||||||
get() = albumWrapper.albumList
|
get() = albumWrapper.albumList
|
||||||
|
@ -10,7 +10,8 @@ class GetPodcastsResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("podcasts") private val channelsWrapper = PodcastChannelWrapper()
|
@JsonProperty("podcasts")
|
||||||
|
private val channelsWrapper = PodcastChannelWrapper()
|
||||||
|
|
||||||
val podcastChannels: List<PodcastChannel>
|
val podcastChannels: List<PodcastChannel>
|
||||||
get() = channelsWrapper.channelsList
|
get() = channelsWrapper.channelsList
|
||||||
|
@ -10,7 +10,8 @@ class GetRandomSongsResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("randomSongs") private val songsWrapper = RandomSongsWrapper()
|
@JsonProperty("randomSongs")
|
||||||
|
private val songsWrapper = RandomSongsWrapper()
|
||||||
|
|
||||||
val songsList
|
val songsList
|
||||||
get() = songsWrapper.songsList
|
get() = songsWrapper.songsList
|
||||||
|
@ -10,7 +10,8 @@ class GetSongsByGenreResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("songsByGenre") private val songsByGenreList = SongsByGenreWrapper()
|
@JsonProperty("songsByGenre")
|
||||||
|
private val songsByGenreList = SongsByGenreWrapper()
|
||||||
|
|
||||||
val songsList get() = songsByGenreList.songsList
|
val songsList get() = songsByGenreList.songsList
|
||||||
}
|
}
|
||||||
|
@ -11,11 +11,13 @@ class JukeboxResponse(
|
|||||||
error: SubsonicError?,
|
error: SubsonicError?,
|
||||||
var jukebox: JukeboxStatus = JukeboxStatus()
|
var jukebox: JukeboxStatus = JukeboxStatus()
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonSetter("jukeboxStatus") fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
@JsonSetter("jukeboxStatus")
|
||||||
|
fun setJukeboxStatus(jukebox: JukeboxStatus) {
|
||||||
this.jukebox = jukebox
|
this.jukebox = jukebox
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonSetter("jukeboxPlaylist") fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
@JsonSetter("jukeboxPlaylist")
|
||||||
|
fun setJukeboxPlaylist(jukebox: JukeboxStatus) {
|
||||||
this.jukebox = jukebox
|
this.jukebox = jukebox
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class MusicFoldersResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("musicFolders") private val wrapper = MusicFoldersWrapper()
|
@JsonProperty("musicFolders")
|
||||||
|
private val wrapper = MusicFoldersWrapper()
|
||||||
|
|
||||||
val musicFolders get() = wrapper.musicFolders
|
val musicFolders get() = wrapper.musicFolders
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ class SharesResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("shares") private val wrappedShares = SharesWrapper()
|
@JsonProperty("shares")
|
||||||
|
private val wrappedShares = SharesWrapper()
|
||||||
|
|
||||||
val shares get() = wrappedShares.share
|
val shares get() = wrappedShares.share
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,8 @@ open class SubsonicResponse(
|
|||||||
) {
|
) {
|
||||||
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
@JsonDeserialize(using = Status.Companion.StatusJsonDeserializer::class)
|
||||||
enum class Status(val jsonValue: String) {
|
enum class Status(val jsonValue: String) {
|
||||||
OK("ok"), ERROR("failed");
|
OK("ok"),
|
||||||
|
ERROR("failed");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getStatusFromJson(jsonValue: String) =
|
fun getStatusFromJson(jsonValue: String) =
|
||||||
|
@ -10,7 +10,8 @@ class VideosResponse(
|
|||||||
version: SubsonicAPIVersions,
|
version: SubsonicAPIVersions,
|
||||||
error: SubsonicError?
|
error: SubsonicError?
|
||||||
) : SubsonicResponse(status, version, error) {
|
) : SubsonicResponse(status, version, error) {
|
||||||
@JsonProperty("videos") private val videosWrapper = VideosWrapper()
|
@JsonProperty("videos")
|
||||||
|
private val videosWrapper = VideosWrapper()
|
||||||
|
|
||||||
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
val videosList: List<MusicDirectoryChild> get() = videosWrapper.videosList
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ class ProxyPasswordInterceptorTest {
|
|||||||
|
|
||||||
private val proxyInterceptor = ProxyPasswordInterceptor(
|
private val proxyInterceptor = ProxyPasswordInterceptor(
|
||||||
V1_12_0,
|
V1_12_0,
|
||||||
mockPasswordHexInterceptor, mockPasswordMd5Interceptor, false
|
mockPasswordHexInterceptor,
|
||||||
|
mockPasswordMd5Interceptor,
|
||||||
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -40,8 +42,10 @@ class ProxyPasswordInterceptorTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `Should use hex password if forceHex is true`() {
|
fun `Should use hex password if forceHex is true`() {
|
||||||
val interceptor = ProxyPasswordInterceptor(
|
val interceptor = ProxyPasswordInterceptor(
|
||||||
V1_16_0, mockPasswordHexInterceptor,
|
V1_16_0,
|
||||||
mockPasswordMd5Interceptor, true
|
mockPasswordHexInterceptor,
|
||||||
|
mockPasswordMd5Interceptor,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
interceptor.intercept(mockChain)
|
interceptor.intercept(mockChain)
|
||||||
|
@ -1,47 +1,46 @@
|
|||||||
[versions]
|
[versions]
|
||||||
# You need to run ./gradlew wrapper after updating the version
|
# You need to run ./gradlew wrapper after updating the version
|
||||||
gradle = "8.1.1"
|
gradle = "8.13"
|
||||||
|
|
||||||
navigation = "2.7.3"
|
navigation = "2.8.9"
|
||||||
gradlePlugin = "8.1.2"
|
gradlePlugin = "8.9.1"
|
||||||
androidxcar = "1.2.0"
|
androidxcar = "1.4.0"
|
||||||
androidxcore = "1.12.0"
|
androidxcore = "1.16.0"
|
||||||
ktlint = "0.43.2"
|
ktlint = "1.0.1"
|
||||||
ktlintGradle = "11.6.0"
|
ktlintGradle = "12.2.0"
|
||||||
detekt = "1.23.0"
|
detekt = "1.23.8"
|
||||||
preferences = "1.2.1"
|
preferences = "1.2.1"
|
||||||
media3 = "1.1.1"
|
media3 = "1.6.1"
|
||||||
|
|
||||||
androidSupport = "1.7.0"
|
androidSupport = "1.9.1"
|
||||||
materialDesign = "1.9.0"
|
materialDesign = "1.12.0"
|
||||||
constraintLayout = "2.1.4"
|
constraintLayout = "2.2.1"
|
||||||
activity = "1.8.0"
|
activity = "1.10.1"
|
||||||
multidex = "2.0.1"
|
multidex = "2.0.1"
|
||||||
room = "2.5.2"
|
room = "2.7.0"
|
||||||
kotlin = "1.9.10"
|
kotlin = "2.1.20"
|
||||||
ksp = "1.9.10-1.0.13"
|
ksp = "2.1.20-2.0.0"
|
||||||
kotlinxCoroutines = "1.7.3"
|
kotlinxCoroutines = "1.10.2"
|
||||||
viewModelKtx = "2.6.2"
|
viewModelKtx = "2.8.7"
|
||||||
swipeRefresh = "1.1.0"
|
swipeRefresh = "1.1.0"
|
||||||
|
|
||||||
retrofit = "2.9.0"
|
retrofit = "2.11.0"
|
||||||
## KEEP ON 2.13 branch (https://github.com/FasterXML/jackson-databind/issues/3658#issuecomment-1312633064) for compatibility with API 24
|
jackson = "2.18.3"
|
||||||
jackson = "2.13.5"
|
okhttp = "4.12.0"
|
||||||
okhttp = "4.11.0"
|
koin = "4.0.4"
|
||||||
koin = "3.5.0"
|
|
||||||
picasso = "2.8"
|
picasso = "2.8"
|
||||||
|
|
||||||
junit4 = "4.13.2"
|
junit4 = "4.13.2"
|
||||||
junit5 = "5.10.0"
|
junit5 = "5.12.2"
|
||||||
mockito = "5.5.0"
|
mockito = "5.17.0"
|
||||||
mockitoKotlin = "5.1.0"
|
mockitoKotlin = "5.4.0"
|
||||||
kluent = "1.73"
|
kluent = "1.73"
|
||||||
apacheCodecs = "1.16.0"
|
apacheCodecs = "1.18.0"
|
||||||
robolectric = "4.10.3"
|
robolectric = "4.14.1"
|
||||||
timber = "5.0.1"
|
timber = "5.0.1"
|
||||||
fastScroll = "2.0.1"
|
fastScroll = "2.0.1"
|
||||||
colorPicker = "2.2.4"
|
colorPicker = "2.3.0"
|
||||||
rxJava = "3.1.8"
|
rxJava = "3.1.10"
|
||||||
rxAndroid = "3.0.2"
|
rxAndroid = "3.0.2"
|
||||||
multiType = "4.3.0"
|
multiType = "4.3.0"
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
ext.versions = [
|
ext.versions = [
|
||||||
minSdk : 21,
|
minSdk : 26,
|
||||||
targetSdk : 33,
|
targetSdk : 33,
|
||||||
compileSdk : 34,
|
compileSdk : 35,
|
||||||
]
|
]
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
20
gradlew
vendored
20
gradlew
vendored
@ -15,6 +15,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@ -55,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (3) This script is generated from the Groovy template
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@ -84,7 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@ -145,7 +147,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
@ -153,7 +155,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
# shellcheck disable=SC3045
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@ -202,11 +204,11 @@ fi
|
|||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
# Collect all arguments for the java command:
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
# and any embedded shellness will be escaped.
|
||||||
# double quotes to make sure that they get re-expanded; and
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
22
gradlew.bat
vendored
22
gradlew.bat
vendored
@ -13,6 +13,8 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
|||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@ -34,10 +34,10 @@ android {
|
|||||||
'minify/proguard-kotlin.pro'
|
'minify/proguard-kotlin.pro'
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled = false
|
||||||
multiDexEnabled true
|
multiDexEnabled = true
|
||||||
testCoverageEnabled true
|
testCoverageEnabled = true
|
||||||
applicationIdSuffix '.debug'
|
applicationIdSuffix = '.debug'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,18 +53,18 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "17"
|
jvmTarget = "21"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding = true
|
||||||
dataBinding true
|
dataBinding = true
|
||||||
buildConfig true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
}
|
}
|
||||||
|
|
||||||
ksp {
|
ksp {
|
||||||
@ -73,8 +73,8 @@ android {
|
|||||||
|
|
||||||
lint {
|
lint {
|
||||||
baseline = file("lint-baseline.xml")
|
baseline = file("lint-baseline.xml")
|
||||||
abortOnError true
|
abortOnError = true
|
||||||
warningsAsErrors true
|
warningsAsErrors = true
|
||||||
warning 'ImpliedQuantity'
|
warning 'ImpliedQuantity'
|
||||||
disable 'IconMissingDensityFolder', 'VectorPath'
|
disable 'IconMissingDensityFolder', 'VectorPath'
|
||||||
disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
disable 'MissingTranslation', 'UnusedQuantity', 'MissingQuantity'
|
||||||
@ -82,10 +82,10 @@ android {
|
|||||||
// We manage dependencies on Gitlab with RenovateBot
|
// We manage dependencies on Gitlab with RenovateBot
|
||||||
disable 'GradleDependency'
|
disable 'GradleDependency'
|
||||||
disable 'AndroidGradlePluginVersion'
|
disable 'AndroidGradlePluginVersion'
|
||||||
textReport true
|
textReport = true
|
||||||
checkDependencies true
|
checkDependencies = true
|
||||||
}
|
}
|
||||||
namespace 'org.moire.ultrasonic'
|
namespace = 'org.moire.ultrasonic'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* NavigationActivity.kt
|
* NavigationActivity.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -13,7 +13,6 @@ import android.content.Intent
|
|||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
@ -96,6 +95,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
private var drawerLayout: DrawerLayout? = null
|
private var drawerLayout: DrawerLayout? = null
|
||||||
private var host: NavHostFragment? = null
|
private var host: NavHostFragment? = null
|
||||||
private var selectServerButton: MaterialButton? = null
|
private var selectServerButton: MaterialButton? = null
|
||||||
|
private var selectServerDropdownImage: ImageView? = null
|
||||||
private var headerBackgroundImage: ImageView? = null
|
private var headerBackgroundImage: ImageView? = null
|
||||||
|
|
||||||
// We store the last search string in this variable.
|
// We store the last search string in this variable.
|
||||||
@ -165,7 +165,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
drawerLayout
|
drawerLayout
|
||||||
)
|
)
|
||||||
|
|
||||||
setupActionBar(navController, appBarConfiguration)
|
setupActionBarWithNavController(navController, appBarConfiguration)
|
||||||
|
|
||||||
setupNavigationMenu(navController)
|
setupNavigationMenu(navController)
|
||||||
|
|
||||||
@ -204,10 +204,11 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
rxBusSubscription += RxBus.playerStateObservable.subscribe {
|
||||||
if (it.state == STATE_READY)
|
if (it.state == STATE_READY) {
|
||||||
showNowPlaying()
|
showNowPlaying()
|
||||||
else
|
} else {
|
||||||
hideNowPlaying()
|
hideNowPlaying()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
rxBusSubscription += RxBus.themeChangedEventObservable.subscribe {
|
||||||
@ -226,7 +227,7 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
// Setup app shortcuts on supported devices, but not on first start, when the server
|
// Setup app shortcuts on supported devices, but not on first start, when the server
|
||||||
// is not configured yet.
|
// is not configured yet.
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1 && !UApp.instance!!.isFirstRun) {
|
if (!UApp.instance!!.isFirstRun) {
|
||||||
ShortcutUtil.registerShortcuts(this)
|
ShortcutUtil.registerShortcuts(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,8 +315,11 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
// Lifecycle support's constructor registers some event receivers so it should be created early
|
// Lifecycle support's constructor registers some event receivers so it should be created early
|
||||||
lifecycleSupport.onCreate()
|
lifecycleSupport.onCreate()
|
||||||
|
|
||||||
if (!nowPlayingHidden) showNowPlaying()
|
if (!nowPlayingHidden) {
|
||||||
else hideNowPlaying()
|
showNowPlaying()
|
||||||
|
} else {
|
||||||
|
hideNowPlaying()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -329,35 +333,31 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateNavigationHeaderForServer() {
|
private fun updateNavigationHeaderForServer() {
|
||||||
// Only show the vector graphic on Android 11 or earlier
|
|
||||||
val showVectorBackground = (Build.VERSION.SDK_INT < Build.VERSION_CODES.S)
|
|
||||||
|
|
||||||
val activeServer = activeServerProvider.getActiveServer()
|
val activeServer = activeServerProvider.getActiveServer()
|
||||||
|
|
||||||
if (cachedServerCount == 0)
|
if (cachedServerCount == 0) {
|
||||||
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
|
selectServerButton?.text = getString(R.string.main_setup_server, activeServer.name)
|
||||||
else selectServerButton?.text = activeServer.name
|
} else {
|
||||||
|
selectServerButton?.text = activeServer.name
|
||||||
|
}
|
||||||
|
|
||||||
val foregroundColor =
|
val foregroundColor =
|
||||||
ServerColor.getForegroundColor(this, activeServer.color, showVectorBackground)
|
ServerColor.getForegroundColor(this, activeServer.color)
|
||||||
val backgroundColor =
|
val backgroundColor =
|
||||||
ServerColor.getBackgroundColor(this, activeServer.color)
|
ServerColor.getBackgroundColor(this, activeServer.color)
|
||||||
|
|
||||||
if (activeServer.index == 0)
|
if (activeServer.index == 0) {
|
||||||
selectServerButton?.icon =
|
selectServerButton?.icon =
|
||||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
|
ContextCompat.getDrawable(this, R.drawable.ic_menu_screen_on_off)
|
||||||
else
|
} else {
|
||||||
selectServerButton?.icon =
|
selectServerButton?.icon =
|
||||||
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
|
ContextCompat.getDrawable(this, R.drawable.ic_menu_select_server)
|
||||||
|
}
|
||||||
|
|
||||||
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
|
selectServerButton?.iconTint = ColorStateList.valueOf(foregroundColor)
|
||||||
selectServerButton?.setTextColor(foregroundColor)
|
selectServerButton?.setTextColor(foregroundColor)
|
||||||
|
selectServerDropdownImage?.imageTintList = ColorStateList.valueOf(foregroundColor)
|
||||||
headerBackgroundImage?.setBackgroundColor(backgroundColor)
|
headerBackgroundImage?.setBackgroundColor(backgroundColor)
|
||||||
|
|
||||||
// Hide the vector graphic on Android 12 or later
|
|
||||||
if (!showVectorBackground) {
|
|
||||||
headerBackgroundImage?.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupNavigationMenu(navController: NavController) {
|
private fun setupNavigationMenu(navController: NavController) {
|
||||||
@ -402,26 +402,23 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
selectServerButton =
|
selectServerButton =
|
||||||
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
|
navigationView?.getHeaderView(0)?.findViewById(R.id.header_select_server)
|
||||||
val dropDownButton: ImageView? =
|
selectServerDropdownImage =
|
||||||
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
|
navigationView?.getHeaderView(0)?.findViewById(R.id.edit_server_button)
|
||||||
|
|
||||||
val onClick: (View) -> Unit = {
|
val onClick: (View) -> Unit = {
|
||||||
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true)
|
if (drawerLayout?.isDrawerVisible(GravityCompat.START) == true) {
|
||||||
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
this.drawerLayout?.closeDrawer(GravityCompat.START)
|
||||||
|
}
|
||||||
navController.navigate(R.id.serverSelectorFragment)
|
navController.navigate(R.id.serverSelectorFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
selectServerButton?.setOnClickListener(onClick)
|
selectServerButton?.setOnClickListener(onClick)
|
||||||
dropDownButton?.setOnClickListener(onClick)
|
selectServerDropdownImage?.setOnClickListener(onClick)
|
||||||
|
|
||||||
headerBackgroundImage =
|
headerBackgroundImage =
|
||||||
navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg)
|
navigationView?.getHeaderView(0)?.findViewById(R.id.img_header_bg)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupActionBar(navController: NavController, appBarConfig: AppBarConfiguration) {
|
|
||||||
setupActionBarWithNavController(navController, appBarConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
|
private val closeNavigationDrawerOnBack = object : OnBackPressedCallback(true) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
drawerLayout?.closeDrawer(GravityCompat.START)
|
drawerLayout?.closeDrawer(GravityCompat.START)
|
||||||
@ -438,19 +435,30 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
return item.onNavDestinationSelected(findNavController(R.id.nav_host_fragment)) ||
|
val navController = findNavController(R.id.nav_host_fragment)
|
||||||
|
// Check if this item ID exists in the nav graph
|
||||||
|
val destinationExists = navController.graph.findNode(item.itemId) != null
|
||||||
|
return if (destinationExists) {
|
||||||
|
item.onNavDestinationSelected(navController) || super.onOptionsItemSelected(item)
|
||||||
|
} else {
|
||||||
|
// Let the fragments handle their own menu items
|
||||||
super.onOptionsItemSelected(item)
|
super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Why is this needed? Shouldn't it just work by default?
|
|
||||||
override fun onSupportNavigateUp(): Boolean {
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration)
|
// This override is required by design when using setupActionBarWithNavController()
|
||||||
|
// with an AppBarConfiguration. It ensures that the Up button behavior is correctly
|
||||||
|
// delegated — either navigating "up" in the back stack, or opening the drawer if
|
||||||
|
// we're at a top-level destination.
|
||||||
|
return findNavController(R.id.nav_host_fragment).navigateUp(appBarConfiguration) ||
|
||||||
|
super.onSupportNavigateUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
|
||||||
when (intent?.action) {
|
when (intent.action) {
|
||||||
Constants.INTENT_PLAY_RANDOM_SONGS -> {
|
Constants.INTENT_PLAY_RANDOM_SONGS -> {
|
||||||
playRandomSongs()
|
playRandomSongs()
|
||||||
}
|
}
|
||||||
@ -473,7 +481,8 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
|
private fun handleSearchIntent(query: String?, autoPlay: Boolean) {
|
||||||
val suggestions = SearchRecentSuggestions(
|
val suggestions = SearchRecentSuggestions(
|
||||||
this,
|
this,
|
||||||
SearchSuggestionProvider.AUTHORITY, SearchSuggestionProvider.MODE
|
SearchSuggestionProvider.AUTHORITY,
|
||||||
|
SearchSuggestionProvider.MODE
|
||||||
)
|
)
|
||||||
suggestions.saveRecentQuery(query, null)
|
suggestions.saveRecentQuery(query, null)
|
||||||
|
|
||||||
@ -528,7 +537,6 @@ class NavigationActivity : ScopeActivity() {
|
|||||||
|
|
||||||
private fun showWelcomeDialog() {
|
private fun showWelcomeDialog() {
|
||||||
if (!UApp.instance!!.setupDialogDisplayed) {
|
if (!UApp.instance!!.setupDialogDisplayed) {
|
||||||
|
|
||||||
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
Settings.firstInstalledVersion = Util.getVersionCode(UApp.applicationContext())
|
||||||
|
|
||||||
InfoDialog.Builder(this)
|
InfoDialog.Builder(this)
|
||||||
|
@ -32,11 +32,11 @@ import org.moire.ultrasonic.util.LayoutType
|
|||||||
*/
|
*/
|
||||||
open class AlbumRowDelegate(
|
open class AlbumRowDelegate(
|
||||||
open val onItemClick: (Album) -> Unit,
|
open val onItemClick: (Album) -> Unit,
|
||||||
open val onContextMenuClick: (MenuItem, Album) -> Boolean,
|
open val onContextMenuClick: (MenuItem, Album) -> Boolean
|
||||||
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
|
) : ItemViewDelegate<Album, AlbumRowDelegate.ListViewHolder>(), KoinComponent {
|
||||||
|
|
||||||
private val starDrawable: Int = R.drawable.ic_star_full
|
private val starDrawable: Int = R.drawable.rating_star_full
|
||||||
private val starHollowDrawable: Int = R.drawable.ic_star_hollow
|
private val starHollowDrawable: Int = R.drawable.rating_star_hollow
|
||||||
|
|
||||||
open var layoutType = LayoutType.LIST
|
open var layoutType = LayoutType.LIST
|
||||||
|
|
||||||
@ -61,8 +61,11 @@ open class AlbumRowDelegate(
|
|||||||
val imageLoaderProvider: ImageLoaderProvider by inject()
|
val imageLoaderProvider: ImageLoaderProvider by inject()
|
||||||
imageLoaderProvider.executeOn {
|
imageLoaderProvider.executeOn {
|
||||||
it.loadImage(
|
it.loadImage(
|
||||||
holder.coverArt, item,
|
holder.coverArt,
|
||||||
false, 0, R.drawable.unknown_album
|
item,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
R.drawable.unknown_album
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,10 +23,7 @@ class DividerBinder : ItemViewBinder<DividerBinder.Divider, DividerBinder.ViewHo
|
|||||||
holder.textView.setText(item.stringId)
|
holder.textView.setText(item.stringId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
|
||||||
inflater: LayoutInflater,
|
|
||||||
parent: ViewGroup
|
|
||||||
): ViewHolder {
|
|
||||||
return ViewHolder(inflater.inflate(layout, parent, false))
|
return ViewHolder(inflater.inflate(layout, parent, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,10 @@ class FolderSelectorBinder(context: Context) :
|
|||||||
val popup = PopupMenu(weakContext.get()!!, layout)
|
val popup = PopupMenu(weakContext.get()!!, layout)
|
||||||
|
|
||||||
var menuItem = popup.menu.add(
|
var menuItem = popup.menu.add(
|
||||||
MENU_GROUP_MUSIC_FOLDER, -1, 0, R.string.select_artist_all_folders
|
MENU_GROUP_MUSIC_FOLDER,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
R.string.select_artist_all_folders
|
||||||
)
|
)
|
||||||
|
|
||||||
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
|
if (selectedFolderId == null || selectedFolderId!!.isEmpty()) {
|
||||||
|
@ -46,7 +46,6 @@ class HeaderViewBinder(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
|
override fun onBindViewHolder(holder: ViewHolder, item: AlbumHeader) {
|
||||||
|
|
||||||
val context = weakContext.get() ?: return
|
val context = weakContext.get() ?: return
|
||||||
val resources = context.resources
|
val resources = context.resources
|
||||||
|
|
||||||
@ -98,7 +97,8 @@ class HeaderViewBinder(
|
|||||||
holder.yearView.text = year
|
holder.yearView.text = year
|
||||||
|
|
||||||
val songs = resources.getQuantityString(
|
val songs = resources.getQuantityString(
|
||||||
R.plurals.n_songs, item.childCount,
|
R.plurals.n_songs,
|
||||||
|
item.childCount,
|
||||||
item.childCount
|
item.childCount
|
||||||
)
|
)
|
||||||
holder.songCountView.text = songs
|
holder.songCountView.text = songs
|
||||||
|
@ -77,7 +77,6 @@ internal class ServerRowAdapter(
|
|||||||
*/
|
*/
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
override fun getView(pos: Int, convertView: View?, parent: ViewGroup?): View? {
|
||||||
|
|
||||||
var vi: View? = convertView
|
var vi: View? = convertView
|
||||||
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
|
if (vi == null) vi = inflater.inflate(R.layout.server_row, parent, false)
|
||||||
|
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
package org.moire.ultrasonic.adapters
|
package org.moire.ultrasonic.adapters
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.MenuInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Checkable
|
import android.widget.Checkable
|
||||||
import android.widget.CheckedTextView
|
import android.widget.CheckedTextView
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
@ -31,6 +36,7 @@ import org.moire.ultrasonic.service.RxBus
|
|||||||
import org.moire.ultrasonic.service.plusAssign
|
import org.moire.ultrasonic.service.plusAssign
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import org.moire.ultrasonic.util.Util.themeColor
|
||||||
|
|
||||||
const val INDICATOR_THICKNESS_INDEFINITE = 5
|
const val INDICATOR_THICKNESS_INDEFINITE = 5
|
||||||
const val INDICATOR_THICKNESS_DEFINITE = 10
|
const val INDICATOR_THICKNESS_DEFINITE = 10
|
||||||
@ -50,17 +56,12 @@ class TrackViewHolder(val view: View) :
|
|||||||
|
|
||||||
var entry: Track? = null
|
var entry: Track? = null
|
||||||
private set
|
private set
|
||||||
var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
|
private var songLayout: LinearLayout = view.findViewById(R.id.song_layout)
|
||||||
|
|
||||||
var check: CheckedTextView = view.findViewById(R.id.song_check)
|
var check: CheckedTextView = view.findViewById(R.id.song_check)
|
||||||
var drag: ImageView = view.findViewById(R.id.song_drag)
|
var drag: ImageView = view.findViewById(R.id.song_drag)
|
||||||
var observableChecked = MutableLiveData(false)
|
var observableChecked = MutableLiveData(false)
|
||||||
|
|
||||||
private var rating: LinearLayout = view.findViewById(R.id.song_rating)
|
|
||||||
private var fiveStar1: ImageView = view.findViewById(R.id.song_five_star_1)
|
|
||||||
private var fiveStar2: ImageView = view.findViewById(R.id.song_five_star_2)
|
|
||||||
private var fiveStar3: ImageView = view.findViewById(R.id.song_five_star_3)
|
|
||||||
private var fiveStar4: ImageView = view.findViewById(R.id.song_five_star_4)
|
|
||||||
private var fiveStar5: ImageView = view.findViewById(R.id.song_five_star_5)
|
|
||||||
private var star: ImageView = view.findViewById(R.id.song_star)
|
private var star: ImageView = view.findViewById(R.id.song_star)
|
||||||
private var track: TextView = view.findViewById(R.id.song_track)
|
private var track: TextView = view.findViewById(R.id.song_track)
|
||||||
private var title: TextView = view.findViewById(R.id.song_title)
|
private var title: TextView = view.findViewById(R.id.song_title)
|
||||||
@ -79,15 +80,34 @@ class TrackViewHolder(val view: View) :
|
|||||||
private var rxBusSubscription: CompositeDisposable? = null
|
private var rxBusSubscription: CompositeDisposable? = null
|
||||||
|
|
||||||
@Suppress("ComplexMethod")
|
@Suppress("ComplexMethod")
|
||||||
fun setSong(
|
fun setSong(song: Track, checkable: Boolean, draggable: Boolean, isSelected: Boolean = false) {
|
||||||
song: Track,
|
|
||||||
checkable: Boolean,
|
|
||||||
draggable: Boolean,
|
|
||||||
isSelected: Boolean = false
|
|
||||||
) {
|
|
||||||
val useFiveStarRating = Settings.useFiveStarRating
|
|
||||||
entry = song
|
entry = song
|
||||||
|
|
||||||
|
// Create new Disposable for the new Subscriptions
|
||||||
|
rxBusSubscription = CompositeDisposable()
|
||||||
|
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
|
||||||
|
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
|
||||||
|
if (it.id != song.id) return@subscribe
|
||||||
|
updateStatus(it.state, it.progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for rating updates
|
||||||
|
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
// Ignore updates which are not for the current song
|
||||||
|
if (it.id != song.id) return@launch
|
||||||
|
|
||||||
|
if (it.rating is HeartRating) {
|
||||||
|
updateRatingDisplay(song.userRating, it.rating.isHeart)
|
||||||
|
} else if (it.rating is StarRating) {
|
||||||
|
updateRatingDisplay(it.rating.starRating.toInt(), song.starred)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val entryDescription = Util.readableEntryDescription(song)
|
val entryDescription = Util.readableEntryDescription(song)
|
||||||
|
|
||||||
artist.text = entryDescription.artist
|
artist.text = entryDescription.artist
|
||||||
@ -108,7 +128,7 @@ class TrackViewHolder(val view: View) :
|
|||||||
if (ActiveServerProvider.isOffline()) {
|
if (ActiveServerProvider.isOffline()) {
|
||||||
star.isGone = true
|
star.isGone = true
|
||||||
} else {
|
} else {
|
||||||
setupStarButtons(song, useFiveStarRating)
|
setupRating(song)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instead of blocking the UI thread while looking up the current state,
|
// Instead of blocking the UI thread while looking up the current state,
|
||||||
@ -120,41 +140,12 @@ class TrackViewHolder(val view: View) :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useFiveStarRating) {
|
updateRatingDisplay(entry!!.userRating, entry!!.starred)
|
||||||
updateFiveStars(entry?.userRating ?: 0)
|
|
||||||
} else {
|
|
||||||
updateSingleStar(entry!!.starred)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (song.isVideo) {
|
if (song.isVideo) {
|
||||||
artist.isGone = true
|
artist.isGone = true
|
||||||
progressIndicator.isGone = true
|
progressIndicator.isGone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new Disposable for the new Subscriptions
|
|
||||||
rxBusSubscription = CompositeDisposable()
|
|
||||||
rxBusSubscription!! += RxBus.playerStateObservable.subscribe {
|
|
||||||
setPlayIcon(it.track?.id == song.id && it.index == bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
rxBusSubscription!! += RxBus.trackDownloadStateObservable.subscribe {
|
|
||||||
if (it.id != song.id) return@subscribe
|
|
||||||
updateStatus(it.state, it.progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for rating updates
|
|
||||||
rxBusSubscription!! += RxBus.ratingPublishedObservable.subscribe {
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
// Ignore updates which are not for the current song
|
|
||||||
if (it.id != song.id) return@launch
|
|
||||||
|
|
||||||
if (it.rating is HeartRating) {
|
|
||||||
updateSingleStar(it.rating.isHeart)
|
|
||||||
} else if (it.rating is StarRating) {
|
|
||||||
updateFiveStars(it.rating.starRating.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is called when the Holder is recycled and receives a new Song
|
// This is called when the Holder is recycled and receives a new Song
|
||||||
@ -171,7 +162,10 @@ class TrackViewHolder(val view: View) :
|
|||||||
if (isPlaying && !isPlayingCached) {
|
if (isPlaying && !isPlayingCached) {
|
||||||
isPlayingCached = true
|
isPlayingCached = true
|
||||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
playingIcon, null, null, null
|
playingIcon,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
)
|
)
|
||||||
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
|
val color = MaterialColors.getColor(view, COLOR_HIGHLIGHT)
|
||||||
songLayout.setBackgroundColor(color)
|
songLayout.setBackgroundColor(color)
|
||||||
@ -179,62 +173,98 @@ class TrackViewHolder(val view: View) :
|
|||||||
} else if (!isPlaying && isPlayingCached) {
|
} else if (!isPlaying && isPlayingCached) {
|
||||||
isPlayingCached = false
|
isPlayingCached = false
|
||||||
title.setCompoundDrawablesWithIntrinsicBounds(
|
title.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
0, 0, 0, 0
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
)
|
)
|
||||||
songLayout.setBackgroundColor(Color.TRANSPARENT)
|
songLayout.setBackgroundColor(Color.TRANSPARENT)
|
||||||
songLayout.elevation = 0F
|
songLayout.elevation = 0F
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupStarButtons(track: Track, useFiveStarRating: Boolean) {
|
private fun setupRating(track: Track) {
|
||||||
if (useFiveStarRating) {
|
star.isVisible = true
|
||||||
// Hide single star
|
updateRatingDisplay(track.userRating, track.starred)
|
||||||
star.isGone = true
|
|
||||||
rating.isVisible = true
|
|
||||||
val rating = if (track.userRating == null) 0 else track.userRating!!
|
|
||||||
updateFiveStars(rating)
|
|
||||||
|
|
||||||
// Five star rating has no click handler because in the
|
star.setOnClickListener { toggleHeart(track) }
|
||||||
// track view theres not enough space
|
star.setOnLongClickListener { view -> showRatingPopup(view, track) }
|
||||||
} else {
|
}
|
||||||
star.isVisible = true
|
|
||||||
rating.isGone = true
|
private fun toggleHeart(track: Track) {
|
||||||
updateSingleStar(track.starred)
|
track.starred = !track.starred
|
||||||
star.setOnClickListener {
|
updateRatingDisplay(track.userRating, track.starred)
|
||||||
track.starred = !track.starred
|
RxBus.ratingSubmitter.onNext(
|
||||||
updateSingleStar(track.starred)
|
RatingUpdate(track.id, HeartRating(track.starred))
|
||||||
RxBus.ratingSubmitter.onNext(
|
)
|
||||||
RatingUpdate(track.id, HeartRating(track.starred))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MagicNumber")
|
@Suppress("MagicNumber")
|
||||||
private fun updateFiveStars(rating: Int) {
|
private fun showRatingPopup(view: View, track: Track): Boolean {
|
||||||
fiveStar1.setImageResource(
|
val popup = PopupMenu(view.context, view)
|
||||||
if (rating > 0) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
val inflater: MenuInflater = popup.menuInflater
|
||||||
)
|
inflater.inflate(R.menu.rating, popup.menu)
|
||||||
fiveStar2.setImageResource(
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) popup.setForceShowIcon(true)
|
||||||
if (rating > 1) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
|
||||||
)
|
popup.setOnMenuItemClickListener {
|
||||||
fiveStar3.setImageResource(
|
val rating = when (it.itemId) {
|
||||||
if (rating > 2) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
R.id.popup_rate_1 -> 1
|
||||||
)
|
R.id.popup_rate_2 -> 2
|
||||||
fiveStar4.setImageResource(
|
R.id.popup_rate_3 -> 3
|
||||||
if (rating > 3) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
R.id.popup_rate_4 -> 4
|
||||||
)
|
R.id.popup_rate_5 -> 5
|
||||||
fiveStar5.setImageResource(
|
else -> 0
|
||||||
if (rating > 4) R.drawable.ic_star_full else R.drawable.ic_star_hollow
|
}
|
||||||
)
|
track.userRating = rating
|
||||||
|
updateRatingDisplay(track.userRating, track.starred)
|
||||||
|
RxBus.ratingSubmitter.onNext(
|
||||||
|
RatingUpdate(track.id, StarRating(5, rating.toFloat()))
|
||||||
|
)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
popup.show()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSingleStar(starred: Boolean) {
|
@Suppress("MagicNumber")
|
||||||
if (starred) {
|
private fun updateRatingDisplay(rating: Int?, starred: Boolean) {
|
||||||
star.setImageResource(R.drawable.ic_star_full)
|
val ratingDrawable = when (rating) {
|
||||||
} else {
|
1 -> R.drawable.rating_star_1
|
||||||
star.setImageResource(R.drawable.ic_star_hollow)
|
2 -> R.drawable.rating_star_2
|
||||||
|
3 -> R.drawable.rating_star_3
|
||||||
|
4 -> R.drawable.rating_star_4
|
||||||
|
5 -> R.drawable.rating_star_5
|
||||||
|
else -> {
|
||||||
|
R.drawable.rating_star_0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val layers = if (starred) {
|
||||||
|
arrayOf(
|
||||||
|
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!,
|
||||||
|
ResourcesCompat.getDrawable(
|
||||||
|
view.resources,
|
||||||
|
R.drawable.rating_heart_mini_overlay,
|
||||||
|
null
|
||||||
|
)!!
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
arrayOf(
|
||||||
|
ResourcesCompat.getDrawable(view.resources, ratingDrawable, null)!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val ratingDisplay = LayerDrawable(layers)
|
||||||
|
ratingDisplay.getDrawable(0).setTint(
|
||||||
|
view.context.themeColor(com.google.android.material.R.attr.colorOnBackground)
|
||||||
|
)
|
||||||
|
if (starred) {
|
||||||
|
ratingDisplay.getDrawable(1).setTint(
|
||||||
|
view.context.themeColor(com.google.android.material.R.attr.colorTertiary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
star.setImageDrawable(ratingDisplay)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateStatus(status: DownloadState, progress: Int?) {
|
private fun updateStatus(status: DownloadState, progress: Int?) {
|
||||||
@ -257,7 +287,8 @@ class TrackViewHolder(val view: View) :
|
|||||||
showProgress()
|
showProgress()
|
||||||
}
|
}
|
||||||
DownloadState.RETRYING,
|
DownloadState.RETRYING,
|
||||||
DownloadState.QUEUED -> {
|
DownloadState.QUEUED
|
||||||
|
-> {
|
||||||
showIndefiniteProgress()
|
showIndefiniteProgress()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -114,10 +114,8 @@ private fun VmPolicy.Builder.detectAllExceptSocket(): VmPolicy.Builder {
|
|||||||
detectLeakedClosableObjects()
|
detectLeakedClosableObjects()
|
||||||
detectLeakedRegistrationObjects()
|
detectLeakedRegistrationObjects()
|
||||||
detectFileUriExposure()
|
detectFileUriExposure()
|
||||||
|
detectContentUriWithoutPermission()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
detectContentUriWithoutPermission()
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
detectCredentialProtectedWhileLocked()
|
detectCredentialProtectedWhileLocked()
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,8 @@ class EqualizerController : CoroutineScope by CoroutineScope(Dispatchers.IO) {
|
|||||||
launch {
|
launch {
|
||||||
try {
|
try {
|
||||||
val settings = deserialize<EqualizerSettings>(
|
val settings = deserialize<EqualizerSettings>(
|
||||||
UApp.applicationContext(), "equalizer.dat"
|
UApp.applicationContext(),
|
||||||
|
"equalizer.dat"
|
||||||
)
|
)
|
||||||
settings?.apply(equalizer!!)
|
settings?.apply(equalizer!!)
|
||||||
} catch (all: Throwable) {
|
} catch (all: Throwable) {
|
||||||
|
@ -51,7 +51,8 @@ class ActiveServerProvider(
|
|||||||
}
|
}
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
|
"getActiveServer retrieved from DataBase, id: %s cachedServer: %s",
|
||||||
serverId, cachedServer
|
serverId,
|
||||||
|
cachedServer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +159,7 @@ class ActiveServerProvider(
|
|||||||
METADATA_DB + serverId
|
METADATA_DB + serverId
|
||||||
)
|
)
|
||||||
.addMigrations(META_MIGRATION_2_3)
|
.addMigrations(META_MIGRATION_2_3)
|
||||||
.fallbackToDestructiveMigrationOnDowngrade()
|
.fallbackToDestructiveMigrationOnDowngrade(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* AppDatabase.kt
|
||||||
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("ktlint:standard:max-line-length")
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
@ -31,8 +40,8 @@ val MIGRATION_1_2: Migration = object : Migration(1, 2) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
@ -48,7 +57,7 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO ServerSettingMigration (
|
INSERT INTO ServerSettingMigration (
|
||||||
id, [index], name, url, userName, password, jukeboxByDefault,
|
id, [index], name, url, userName, password, jukeboxByDefault,
|
||||||
@ -60,10 +69,10 @@ val MIGRATION_2_1: Migration = object : Migration(2, 1) {
|
|||||||
FROM ServerSetting
|
FROM ServerSetting
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"DROP TABLE ServerSetting"
|
"DROP TABLE ServerSetting"
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -87,8 +96,8 @@ val MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
@ -105,7 +114,7 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO ServerSettingMigration (
|
INSERT INTO ServerSettingMigration (
|
||||||
id, [index], name, url, userName, password, jukeboxByDefault,
|
id, [index], name, url, userName, password, jukeboxByDefault,
|
||||||
@ -117,26 +126,26 @@ val MIGRATION_3_2: Migration = object : Migration(3, 2) {
|
|||||||
FROM ServerSetting
|
FROM ServerSetting
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"DROP TABLE ServerSetting"
|
"DROP TABLE ServerSetting"
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
|
val MIGRATION_3_4: Migration = object : Migration(3, 4) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"ALTER TABLE ServerSetting ADD COLUMN color INTEGER"
|
"ALTER TABLE ServerSetting ADD COLUMN color INTEGER"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
CREATE TABLE IF NOT EXISTS ServerSettingMigration (
|
||||||
id INTEGER NOT NULL PRIMARY KEY,
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
@ -157,7 +166,7 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO ServerSettingMigration (
|
INSERT INTO ServerSettingMigration (
|
||||||
id, [index], name, url, userName, password, jukeboxByDefault,
|
id, [index], name, url, userName, password, jukeboxByDefault,
|
||||||
@ -171,18 +180,18 @@ val MIGRATION_4_3: Migration = object : Migration(4, 3) {
|
|||||||
FROM ServerSetting
|
FROM ServerSetting
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"DROP TABLE ServerSetting"
|
"DROP TABLE ServerSetting"
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
"ALTER TABLE ServerSettingMigration RENAME TO ServerSetting"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
||||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
@ -204,7 +213,7 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO `_new_ServerSetting` (
|
INSERT INTO `_new_ServerSetting` (
|
||||||
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
||||||
@ -218,14 +227,14 @@ val MIGRATION_4_5: Migration = object : Migration(4, 5) {
|
|||||||
FROM `ServerSetting`
|
FROM `ServerSetting`
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL("DROP TABLE `ServerSetting`")
|
db.execSQL("DROP TABLE `ServerSetting`")
|
||||||
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (
|
||||||
`id` INTEGER PRIMARY KEY NOT NULL,
|
`id` INTEGER PRIMARY KEY NOT NULL,
|
||||||
@ -247,7 +256,7 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
|||||||
)
|
)
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL(
|
db.execSQL(
|
||||||
"""
|
"""
|
||||||
INSERT INTO `_new_ServerSetting` (
|
INSERT INTO `_new_ServerSetting` (
|
||||||
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
`ldapSupport`,`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,
|
||||||
@ -261,25 +270,31 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
|
|||||||
FROM `ServerSetting`
|
FROM `ServerSetting`
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
)
|
)
|
||||||
database.execSQL("DROP TABLE `ServerSetting`")
|
db.execSQL("DROP TABLE `ServerSetting`")
|
||||||
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ktlint-disable max-line-length */
|
|
||||||
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
|
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)")
|
db.execSQL(
|
||||||
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
|
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `forcePlainTextPassword` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER, `jukeboxSupport` INTEGER, `videoSupport` INTEGER)"
|
||||||
database.execSQL("DROP TABLE `ServerSetting`")
|
)
|
||||||
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
db.execSQL(
|
||||||
|
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
|
||||||
|
)
|
||||||
|
db.execSQL("DROP TABLE `ServerSetting`")
|
||||||
|
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
|
val MIGRATION_6_5: Migration = object : Migration(6, 5) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)")
|
db.execSQL(
|
||||||
database.execSQL("INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`")
|
"CREATE TABLE IF NOT EXISTS `_new_ServerSetting` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `index` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `color` INTEGER, `userName` TEXT NOT NULL, `password` TEXT NOT NULL, `jukeboxByDefault` INTEGER NOT NULL, `allowSelfSignedCertificate` INTEGER NOT NULL, `ldapSupport` INTEGER NOT NULL, `musicFolderId` TEXT, `minimumApiVersion` TEXT, `chatSupport` INTEGER, `bookmarkSupport` INTEGER, `shareSupport` INTEGER, `podcastSupport` INTEGER)"
|
||||||
database.execSQL("DROP TABLE `ServerSetting`")
|
)
|
||||||
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
db.execSQL(
|
||||||
|
"INSERT INTO `_new_ServerSetting` (`musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`ldapSupport`,`id`,`allowSelfSignedCertificate`,`chatSupport`) SELECT `musicFolderId`,`color`,`index`,`userName`,`minimumApiVersion`,`jukeboxByDefault`,`url`,`password`,`shareSupport`,`bookmarkSupport`,`name`,`podcastSupport`,`forcePlainTextPassword`,`id`,`allowSelfSignedCertificate`,`chatSupport` FROM `ServerSetting`"
|
||||||
|
)
|
||||||
|
db.execSQL("DROP TABLE `ServerSetting`")
|
||||||
|
db.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ktlint-enable max-line-length */
|
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* BasicDaos.kt
|
||||||
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
@ -91,7 +98,7 @@ interface GenericDao<T> {
|
|||||||
*/
|
*/
|
||||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
@JvmSuppressWildcards
|
@JvmSuppressWildcards
|
||||||
fun insertIgnoring(obj: List<T>?): List<Long>
|
fun insertIgnoring(obj: List<T>): List<Long>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update an object from the database.
|
* Update an object from the database.
|
||||||
@ -109,7 +116,7 @@ interface GenericDao<T> {
|
|||||||
*/
|
*/
|
||||||
@Update
|
@Update
|
||||||
@JvmSuppressWildcards
|
@JvmSuppressWildcards
|
||||||
fun update(obj: List<T>?)
|
fun update(obj: List<T>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete an object from the database
|
* Delete an object from the database
|
||||||
|
@ -39,9 +39,7 @@ class CachedDataSource(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDataSourceInternal(
|
private fun createDataSourceInternal(upstreamDataSource: DataSource): CachedDataSource {
|
||||||
upstreamDataSource: DataSource
|
|
||||||
): CachedDataSource {
|
|
||||||
return CachedDataSource(
|
return CachedDataSource(
|
||||||
upstreamDataSource
|
upstreamDataSource
|
||||||
)
|
)
|
||||||
@ -93,7 +91,9 @@ class CachedDataSource(
|
|||||||
readInternal(buffer, offset, length)
|
readInternal(buffer, offset, length)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw HttpDataSourceException.createForIOException(
|
throw HttpDataSourceException.createForIOException(
|
||||||
e, Util.castNonNull(dataSpec), HttpDataSourceException.TYPE_READ
|
e,
|
||||||
|
Util.castNonNull(dataSpec),
|
||||||
|
HttpDataSourceException.TYPE_READ
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
/*
|
/*
|
||||||
* MetaDatabase.kt
|
* MetaDatabase.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("ktlint:standard:max-line-length")
|
||||||
|
|
||||||
package org.moire.ultrasonic.data
|
package org.moire.ultrasonic.data
|
||||||
|
|
||||||
import androidx.room.AutoMigration
|
import androidx.room.AutoMigration
|
||||||
@ -37,7 +39,7 @@ import org.moire.ultrasonic.domain.Track
|
|||||||
AutoMigration(
|
AutoMigration(
|
||||||
from = 1,
|
from = 1,
|
||||||
to = 2
|
to = 2
|
||||||
),
|
)
|
||||||
],
|
],
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
version = 3
|
version = 3
|
||||||
@ -67,19 +69,27 @@ class Converters {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ktlint-disable max-line-length */
|
|
||||||
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
val META_MIGRATION_2_3: Migration = object : Migration(2, 3) {
|
||||||
override fun migrate(database: SupportSQLiteDatabase) {
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
database.execSQL("DROP TABLE `albums`")
|
db.execSQL("DROP TABLE `albums`")
|
||||||
database.execSQL("DROP TABLE `indexes`")
|
db.execSQL("DROP TABLE `indexes`")
|
||||||
database.execSQL("DROP TABLE `artists`")
|
db.execSQL("DROP TABLE `artists`")
|
||||||
database.execSQL("DROP TABLE `tracks`")
|
db.execSQL("DROP TABLE `tracks`")
|
||||||
database.execSQL("DROP TABLE `music_folders`")
|
db.execSQL("DROP TABLE `music_folders`")
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
db.execSQL(
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
"CREATE TABLE IF NOT EXISTS `albums` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `album` TEXT, `title` TEXT, `name` TEXT, `discNumber` INTEGER, `coverArt` TEXT, `songCount` INTEGER, `created` INTEGER, `artist` TEXT, `artistId` TEXT, `duration` INTEGER, `year` INTEGER, `genre` TEXT, `starred` INTEGER NOT NULL, `path` TEXT, `closeness` INTEGER NOT NULL, `isDirectory` INTEGER NOT NULL, `isVideo` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))")
|
)
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))")
|
db.execSQL(
|
||||||
database.execSQL("CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))")
|
"CREATE TABLE IF NOT EXISTS `indexes` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, `musicFolderId` TEXT, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `artists` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `name` TEXT, `index` TEXT, `coverArt` TEXT, `albumCount` INTEGER, `closeness` INTEGER NOT NULL, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `music_folders` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `tracks` (`id` TEXT NOT NULL, `serverId` INTEGER NOT NULL DEFAULT -1, `parent` TEXT, `isDirectory` INTEGER NOT NULL, `title` TEXT, `album` TEXT, `albumId` TEXT, `artist` TEXT, `artistId` TEXT, `track` INTEGER, `year` INTEGER, `genre` TEXT, `contentType` TEXT, `suffix` TEXT, `transcodedContentType` TEXT, `transcodedSuffix` TEXT, `coverArt` TEXT, `size` INTEGER, `songCount` INTEGER, `duration` INTEGER, `bitRate` INTEGER, `path` TEXT, `isVideo` INTEGER NOT NULL, `starred` INTEGER NOT NULL, `discNumber` INTEGER, `type` TEXT, `created` INTEGER, `closeness` INTEGER NOT NULL, `bookmarkPosition` INTEGER NOT NULL, `userRating` INTEGER, `averageRating` REAL, `name` TEXT, PRIMARY KEY(`id`, `serverId`))"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* ktlint-enable max-line-length */
|
|
||||||
|
@ -2,7 +2,7 @@ package org.moire.ultrasonic.di
|
|||||||
|
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.core.module.dsl.viewModel
|
||||||
import org.koin.core.qualifier.named
|
import org.koin.core.qualifier.named
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.data.AppDatabase
|
import org.moire.ultrasonic.data.AppDatabase
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.moire.ultrasonic.activity.NavigationActivity
|
|
||||||
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
import org.moire.ultrasonic.service.ExternalStorageMonitor
|
||||||
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
import org.moire.ultrasonic.service.MediaPlayerLifecycleSupport
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.MediaPlayerManager
|
||||||
@ -20,8 +19,7 @@ val mediaPlayerModule = module {
|
|||||||
single { NetworkAndStorageChecker() }
|
single { NetworkAndStorageChecker() }
|
||||||
single { ShareHandler() }
|
single { ShareHandler() }
|
||||||
|
|
||||||
scope<NavigationActivity> {
|
// These MUST be singletons, for the media playback must work headless (without an activity)
|
||||||
scoped { MediaPlayerManager(get(), get()) }
|
single { MediaPlayerManager(get(), get()) }
|
||||||
scoped { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
|
single { MediaPlayerLifecycleSupport(get(), get(), get(), get()) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
@file:JvmName("MusicServiceModule")
|
@file:JvmName("MusicServiceModule")
|
||||||
|
|
||||||
package org.moire.ultrasonic.di
|
package org.moire.ultrasonic.di
|
||||||
|
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Album entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIAlbumConverter")
|
@file:JvmName("APIAlbumConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Album
|
import org.moire.ultrasonic.api.subsonic.models.Album
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Artist entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIArtistConverter")
|
@file:JvmName("APIArtistConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
import org.moire.ultrasonic.api.subsonic.models.Artist as APIArtist
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Contains helper functions to convert from api ChatMessage entity to domain entity
|
// Contains helper functions to convert from api ChatMessage entity to domain entity
|
||||||
@file:JvmName("APIChatMessageConverter")
|
@file:JvmName("APIChatMessageConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage
|
import org.moire.ultrasonic.api.subsonic.models.ChatMessage as ApiChatMessage
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Collection of functions to convert api Genre entity to domain entity
|
// Collection of functions to convert api Genre entity to domain entity
|
||||||
@file:JvmName("ApiGenreConverter")
|
@file:JvmName("ApiGenreConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre
|
import org.moire.ultrasonic.api.subsonic.models.Genre as APIGenre
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Collection of function to convert subsonic api jukebox responses to app entities
|
// Collection of function to convert subsonic api jukebox responses to app entities
|
||||||
@file:JvmName("APIJukeboxConverter")
|
@file:JvmName("APIJukeboxConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus
|
import org.moire.ultrasonic.api.subsonic.models.JukeboxStatus as ApiJukeboxStatus
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts Lyrics entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APILyricsConverter")
|
@file:JvmName("APILyricsConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics
|
import org.moire.ultrasonic.api.subsonic.models.Lyrics as APILyrics
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
@file:JvmName("APIMusicDirectoryConverter")
|
@file:JvmName("APIMusicDirectoryConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
@ -35,10 +36,7 @@ fun MusicDirectoryChild.toAlbumEntity(serverId: Int): Album = Album(id, serverId
|
|||||||
populateCommonProps(this, this@toAlbumEntity)
|
populateCommonProps(this, this@toAlbumEntity)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateCommonProps(
|
private fun populateCommonProps(entry: MusicDirectory.Child, source: MusicDirectoryChild) {
|
||||||
entry: MusicDirectory.Child,
|
|
||||||
source: MusicDirectoryChild
|
|
||||||
) {
|
|
||||||
entry.parent = source.parent
|
entry.parent = source.parent
|
||||||
entry.isDirectory = source.isDir
|
entry.isDirectory = source.isDir
|
||||||
entry.title = source.title
|
entry.title = source.title
|
||||||
@ -63,10 +61,7 @@ private fun populateCommonProps(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun populateTrackProps(
|
private fun populateTrackProps(track: Track, source: MusicDirectoryChild) {
|
||||||
track: Track,
|
|
||||||
source: MusicDirectoryChild
|
|
||||||
) {
|
|
||||||
track.size = source.size
|
track.size = source.size
|
||||||
track.contentType = source.contentType
|
track.contentType = source.contentType
|
||||||
track.suffix = source.suffix
|
track.suffix = source.suffix
|
||||||
@ -84,10 +79,11 @@ fun List<MusicDirectoryChild>.toDomainEntityList(serverId: Int): List<MusicDirec
|
|||||||
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
val newList: MutableList<MusicDirectory.Child> = mutableListOf()
|
||||||
|
|
||||||
forEach {
|
forEach {
|
||||||
if (it.isDir)
|
if (it.isDir) {
|
||||||
newList.add(it.toAlbumEntity(serverId))
|
newList.add(it.toAlbumEntity(serverId))
|
||||||
else
|
} else {
|
||||||
newList.add(it.toTrackEntity(serverId))
|
newList.add(it.toTrackEntity(serverId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newList
|
return newList
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts MusicFolder entity from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIMusicFolderConverter")
|
@file:JvmName("APIMusicFolderConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
import org.moire.ultrasonic.api.subsonic.models.MusicFolder as APIMusicFolder
|
||||||
@ -18,9 +19,8 @@ fun APIMusicFolder.toDomainEntity(serverId: Int): MusicFolder = MusicFolder(
|
|||||||
name = this.name
|
name = this.name
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> =
|
fun List<APIMusicFolder>.toDomainEntityList(serverId: Int): List<MusicFolder> = this.map {
|
||||||
this.map {
|
val item = it.toDomainEntity(serverId)
|
||||||
val item = it.toDomainEntity(serverId)
|
item.serverId = serverId
|
||||||
item.serverId = serverId
|
item
|
||||||
item
|
}
|
||||||
}
|
|
||||||
|
@ -31,8 +31,11 @@ fun APIPlaylist.toMusicDirectoryDomainEntity(serverId: Int): MusicDirectory =
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
fun APIPlaylist.toDomainEntity(): Playlist = Playlist(
|
||||||
this.id, this.name, this.owner,
|
this.id,
|
||||||
this.comment, this.songCount.toString(),
|
this.name,
|
||||||
|
this.owner,
|
||||||
|
this.comment,
|
||||||
|
this.songCount.toString(),
|
||||||
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
|
this.created.ifNotNull { playlistDateFormat.format(it.time) } ?: "",
|
||||||
public
|
public
|
||||||
)
|
)
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts podcasts entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APIPodcastConverter")
|
@file:JvmName("APIPodcastConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
|
import org.moire.ultrasonic.api.subsonic.models.PodcastChannel
|
||||||
|
|
||||||
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
|
fun PodcastChannel.toDomainEntity(): PodcastsChannel = PodcastsChannel(
|
||||||
this.id, this.title, this.url, this.description, this.status
|
this.id,
|
||||||
|
this.title,
|
||||||
|
this.url,
|
||||||
|
this.description,
|
||||||
|
this.status
|
||||||
)
|
)
|
||||||
|
|
||||||
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this
|
fun List<PodcastChannel>.toDomainEntitiesList(): List<PodcastsChannel> = this
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
// Converts SearchResult entities from [org.moire.ultrasonic.api.subsonic.SubsonicAPIClient]
|
||||||
// to app domain entities.
|
// to app domain entities.
|
||||||
@file:JvmName("APISearchConverter")
|
@file:JvmName("APISearchConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchResult as APISearchResult
|
||||||
@ -15,7 +16,8 @@ import org.moire.ultrasonic.api.subsonic.models.SearchThreeResult
|
|||||||
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
import org.moire.ultrasonic.api.subsonic.models.SearchTwoResult
|
||||||
|
|
||||||
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
fun APISearchResult.toDomainEntity(serverId: Int): SearchResult = SearchResult(
|
||||||
emptyList(), emptyList(),
|
emptyList(),
|
||||||
|
emptyList(),
|
||||||
this.matchList.map { it.toTrackEntity(serverId) }
|
this.matchList.map { it.toTrackEntity(serverId) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
// Contains helper method to convert subsonic api share to domain model
|
// Contains helper method to convert subsonic api share to domain model
|
||||||
@file:JvmName("APIShareConverter")
|
@file:JvmName("APIShareConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
// Helper functions to convert User entity to domain entity
|
// Helper functions to convert User entity to domain entity
|
||||||
@file:JvmName("APIUserConverter")
|
@file:JvmName("APIUserConverter")
|
||||||
|
|
||||||
package org.moire.ultrasonic.domain
|
package org.moire.ultrasonic.domain
|
||||||
|
|
||||||
import org.moire.ultrasonic.api.subsonic.models.User
|
import org.moire.ultrasonic.api.subsonic.models.User
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* AboutFragment.kt
|
* AboutFragment.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -8,13 +8,13 @@
|
|||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
@ -60,13 +60,13 @@ class AboutFragment : Fragment() {
|
|||||||
|
|
||||||
webPageButton?.setOnClickListener {
|
webPageButton?.setOnClickListener {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_webpage_url)))
|
Intent(Intent.ACTION_VIEW, getString(R.string.about_webpage_url).toUri())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
reportBugButton?.setOnClickListener {
|
reportBugButton?.setOnClickListener {
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.about_report_url)))
|
Intent(Intent.ACTION_VIEW, getString(R.string.about_report_url).toUri())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* AlbumListFragment.kt
|
* AlbumListFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -65,10 +65,7 @@ class AlbumListFragment(
|
|||||||
/**
|
/**
|
||||||
* The central function to pass a query to the model and return a LiveData object
|
* The central function to pass a query to the model and return a LiveData object
|
||||||
*/
|
*/
|
||||||
override fun getLiveData(
|
override fun getLiveData(refresh: Boolean, append: Boolean): LiveData<List<Album>> {
|
||||||
refresh: Boolean,
|
|
||||||
append: Boolean
|
|
||||||
): LiveData<List<Album>> {
|
|
||||||
fetchAlbums(refresh)
|
fetchAlbums(refresh)
|
||||||
|
|
||||||
return listModel.list
|
return listModel.list
|
||||||
|
@ -17,7 +17,6 @@ import org.moire.ultrasonic.NavigationGraphDirections
|
|||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
import org.moire.ultrasonic.adapters.ArtistRowBinder
|
||||||
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
import org.moire.ultrasonic.api.subsonic.models.AlbumListType
|
||||||
import org.moire.ultrasonic.domain.Artist
|
|
||||||
import org.moire.ultrasonic.domain.ArtistOrIndex
|
import org.moire.ultrasonic.domain.ArtistOrIndex
|
||||||
import org.moire.ultrasonic.domain.Index
|
import org.moire.ultrasonic.domain.Index
|
||||||
import org.moire.ultrasonic.model.ArtistListModel
|
import org.moire.ultrasonic.model.ArtistListModel
|
||||||
@ -70,7 +69,7 @@ class ArtistListFragment : EntryListFragment<ArtistOrIndex>() {
|
|||||||
id = item.id,
|
id = item.id,
|
||||||
name = item.name,
|
name = item.name,
|
||||||
parentId = item.id,
|
parentId = item.id,
|
||||||
isArtist = (item is Artist)
|
isArtist = false
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
NavigationGraphDirections.toAlbumList(
|
NavigationGraphDirections.toAlbumList(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* BookmarksFragment.kt
|
* BookmarksFragment.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -68,7 +68,6 @@ class BookmarksFragment : TrackCollectionFragment() {
|
|||||||
*/
|
*/
|
||||||
private fun playNow(songs: List<Track>) {
|
private fun playNow(songs: List<Track>) {
|
||||||
if (songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
|
|
||||||
mediaPlayerManager.addToPlaylist(
|
mediaPlayerManager.addToPlaylist(
|
||||||
songs = songs,
|
songs = songs,
|
||||||
autoPlay = false,
|
autoPlay = false,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* EditServerFragment.kt
|
* EditServerFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -86,7 +86,8 @@ class EditServerFragment : Fragment() {
|
|||||||
|
|
||||||
override fun onAttach(context: Context) {
|
override fun onAttach(context: Context) {
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(
|
requireActivity().onBackPressedDispatcher.addCallback(
|
||||||
this, confirmCloseCallback
|
this,
|
||||||
|
confirmCloseCallback
|
||||||
)
|
)
|
||||||
super.onAttach(context)
|
super.onAttach(context)
|
||||||
}
|
}
|
||||||
@ -186,7 +187,7 @@ class EditServerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
.setNegativeButton(getString(R.string.common_cancel)) {
|
.setNegativeButton(getString(R.string.common_cancel)) {
|
||||||
dialogInterface, _ ->
|
dialogInterface, _ ->
|
||||||
dialogInterface.dismiss()
|
dialogInterface.dismiss()
|
||||||
}
|
}
|
||||||
.setBottomSpace(DIALOG_PADDING)
|
.setBottomSpace(DIALOG_PADDING)
|
||||||
@ -199,7 +200,8 @@ class EditServerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val confirmCloseCallback = object : OnBackPressedCallback(
|
private val confirmCloseCallback = object : OnBackPressedCallback(
|
||||||
true // default to enabled
|
// default to enabled
|
||||||
|
true
|
||||||
) {
|
) {
|
||||||
override fun handleOnBackPressed() {
|
override fun handleOnBackPressed() {
|
||||||
finishActivity()
|
finishActivity()
|
||||||
@ -231,35 +233,46 @@ class EditServerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
override fun onSaveInstanceState(savedInstanceState: Bundle) {
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::serverNameEditText.name, serverNameEditText!!.editText?.text.toString()
|
::serverNameEditText.name,
|
||||||
|
serverNameEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::serverAddressEditText.name, serverAddressEditText!!.editText?.text.toString()
|
::serverAddressEditText.name,
|
||||||
|
serverAddressEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::userNameEditText.name, userNameEditText!!.editText?.text.toString()
|
::userNameEditText.name,
|
||||||
|
userNameEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putString(
|
savedInstanceState.putString(
|
||||||
::passwordEditText.name, passwordEditText!!.editText?.text.toString()
|
::passwordEditText.name,
|
||||||
|
passwordEditText!!.editText?.text.toString()
|
||||||
)
|
)
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::selfSignedSwitch.name, selfSignedSwitch!!.isChecked
|
::selfSignedSwitch.name,
|
||||||
|
selfSignedSwitch!!.isChecked
|
||||||
)
|
)
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::plaintextSwitch.name, plaintextSwitch!!.isChecked
|
::plaintextSwitch.name,
|
||||||
|
plaintextSwitch!!.isChecked
|
||||||
)
|
)
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::jukeboxSwitch.name, jukeboxSwitch!!.isChecked
|
::jukeboxSwitch.name,
|
||||||
|
jukeboxSwitch!!.isChecked
|
||||||
)
|
)
|
||||||
savedInstanceState.putInt(
|
savedInstanceState.putInt(
|
||||||
::serverColorImageView.name, currentColor
|
::serverColorImageView.name,
|
||||||
|
currentColor
|
||||||
)
|
)
|
||||||
if (selectedColor != null)
|
if (selectedColor != null) {
|
||||||
savedInstanceState.putInt(
|
savedInstanceState.putInt(
|
||||||
::selectedColor.name, selectedColor!!
|
::selectedColor.name,
|
||||||
|
selectedColor!!
|
||||||
)
|
)
|
||||||
|
}
|
||||||
savedInstanceState.putBoolean(
|
savedInstanceState.putBoolean(
|
||||||
::isInstanceStateSaved.name, true
|
::isInstanceStateSaved.name,
|
||||||
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
super.onSaveInstanceState(savedInstanceState)
|
super.onSaveInstanceState(savedInstanceState)
|
||||||
@ -286,8 +299,9 @@ class EditServerFragment : Fragment() {
|
|||||||
plaintextSwitch!!.isChecked = savedInstanceState.getBoolean(::plaintextSwitch.name)
|
plaintextSwitch!!.isChecked = savedInstanceState.getBoolean(::plaintextSwitch.name)
|
||||||
jukeboxSwitch!!.isChecked = savedInstanceState.getBoolean(::jukeboxSwitch.name)
|
jukeboxSwitch!!.isChecked = savedInstanceState.getBoolean(::jukeboxSwitch.name)
|
||||||
updateColor(savedInstanceState.getInt(::serverColorImageView.name))
|
updateColor(savedInstanceState.getInt(::serverColorImageView.name))
|
||||||
if (savedInstanceState.containsKey(::selectedColor.name))
|
if (savedInstanceState.containsKey(::selectedColor.name)) {
|
||||||
selectedColor = savedInstanceState.getInt(::selectedColor.name)
|
selectedColor = savedInstanceState.getInt(::selectedColor.name)
|
||||||
|
}
|
||||||
isInstanceStateSaved = savedInstanceState.getBoolean(::isInstanceStateSaved.name)
|
isInstanceStateSaved = savedInstanceState.getBoolean(::isInstanceStateSaved.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -434,7 +448,7 @@ class EditServerFragment : Fragment() {
|
|||||||
serverSetting.shareSupport,
|
serverSetting.shareSupport,
|
||||||
serverSetting.podcastSupport,
|
serverSetting.podcastSupport,
|
||||||
serverSetting.videoSupport,
|
serverSetting.videoSupport,
|
||||||
serverSetting.jukeboxSupport,
|
serverSetting.jukeboxSupport
|
||||||
).any { x -> x == false }
|
).any { x -> x == false }
|
||||||
|
|
||||||
var progressString = String.format(
|
var progressString = String.format(
|
||||||
@ -445,7 +459,7 @@ class EditServerFragment : Fragment() {
|
|||||||
|%s - ${resources.getString(R.string.button_bar_podcasts)}
|
|%s - ${resources.getString(R.string.button_bar_podcasts)}
|
||||||
|%s - ${resources.getString(R.string.main_videos)}
|
|%s - ${resources.getString(R.string.main_videos)}
|
||||||
|%s - ${resources.getString(R.string.jukebox)}
|
|%s - ${resources.getString(R.string.jukebox)}
|
||||||
""".trimMargin(),
|
""".trimMargin(),
|
||||||
boolToMark(serverSetting.chatSupport),
|
boolToMark(serverSetting.chatSupport),
|
||||||
boolToMark(serverSetting.bookmarkSupport),
|
boolToMark(serverSetting.bookmarkSupport),
|
||||||
boolToMark(serverSetting.shareSupport),
|
boolToMark(serverSetting.shareSupport),
|
||||||
@ -453,15 +467,17 @@ class EditServerFragment : Fragment() {
|
|||||||
boolToMark(serverSetting.videoSupport),
|
boolToMark(serverSetting.videoSupport),
|
||||||
boolToMark(serverSetting.jukeboxSupport)
|
boolToMark(serverSetting.jukeboxSupport)
|
||||||
)
|
)
|
||||||
if (isAnyDisabled)
|
if (isAnyDisabled) {
|
||||||
progressString += "\n\n" + resources.getString(R.string.server_editor_disabled_feature)
|
progressString += "\n\n" + resources.getString(R.string.server_editor_disabled_feature)
|
||||||
|
}
|
||||||
|
|
||||||
return progressString
|
return progressString
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun boolToMark(value: Boolean?): String {
|
private fun boolToMark(value: Boolean?): String {
|
||||||
if (value == null)
|
if (value == null) {
|
||||||
return "⌛"
|
return "⌛"
|
||||||
|
}
|
||||||
return if (value) "✔️" else "❌"
|
return if (value) "✔️" else "❌"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ abstract class EntryListFragment<T : GenericEntry> : MultiListFragment<T>(), Koi
|
|||||||
id = item.id,
|
id = item.id,
|
||||||
name = item.name,
|
name = item.name,
|
||||||
parentId = item.id,
|
parentId = item.id,
|
||||||
isArtist = (item is Artist),
|
isArtist = (item is Artist)
|
||||||
)
|
)
|
||||||
|
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
|
@ -92,7 +92,9 @@ class EqualizerFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
for (preset in 0 until equalizer!!.numberOfPresets) {
|
for (preset in 0 until equalizer!!.numberOfPresets) {
|
||||||
val menuItem = menu.add(
|
val menuItem = menu.add(
|
||||||
MENU_GROUP_PRESET, preset, preset,
|
MENU_GROUP_PRESET,
|
||||||
|
preset,
|
||||||
|
preset,
|
||||||
equalizer!!.getPresetName(
|
equalizer!!.getPresetName(
|
||||||
preset.toShort()
|
preset.toShort()
|
||||||
)
|
)
|
||||||
@ -188,11 +190,7 @@ class EqualizerFragment : Fragment() {
|
|||||||
updateLevelText(levelTextView, bandLevel)
|
updateLevelText(levelTextView, bandLevel)
|
||||||
|
|
||||||
bar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
bar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||||
override fun onProgressChanged(
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
seekBar: SeekBar,
|
|
||||||
progress: Int,
|
|
||||||
fromUser: Boolean
|
|
||||||
) {
|
|
||||||
val level = (progress + minEQLevel).toShort()
|
val level = (progress + minEQLevel).toShort()
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
try {
|
try {
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* FragmentTitle.kt
|
||||||
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
@ -58,7 +58,6 @@ class MainFragment : ScopeFragment(), KoinScopeComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
FragmentTitle.setTitle(this, R.string.music_library_label)
|
FragmentTitle.setTitle(this, R.string.music_library_label)
|
||||||
|
|
||||||
// Load last layout from settings
|
// Load last layout from settings
|
||||||
@ -133,10 +132,7 @@ class MainFragment : ScopeFragment(), KoinScopeComponent {
|
|||||||
return findFragmentAtPosition(childFragmentManager, viewPager.currentItem)
|
return findFragmentAtPosition(childFragmentManager, viewPager.currentItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findFragmentAtPosition(
|
private fun findFragmentAtPosition(fragmentManager: FragmentManager, position: Int): Fragment? {
|
||||||
fragmentManager: FragmentManager,
|
|
||||||
position: Int
|
|
||||||
): Fragment? {
|
|
||||||
// If a fragment was recently created and never shown the fragment manager might not
|
// If a fragment was recently created and never shown the fragment manager might not
|
||||||
// hold a reference to it. Fallback on the WeakMap instead.
|
// hold a reference to it. Fallback on the WeakMap instead.
|
||||||
return fragmentManager.findFragmentByTag("f$position")
|
return fragmentManager.findFragmentByTag("f$position")
|
||||||
@ -172,7 +168,6 @@ class MusicCollectionAdapter(fragment: Fragment, initialType: LayoutType = Layou
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
override fun createFragment(position: Int): Fragment {
|
||||||
|
|
||||||
Timber.i("Creating new fragment at position: $position")
|
Timber.i("Creating new fragment at position: $position")
|
||||||
|
|
||||||
val action = when (position) {
|
val action = when (position) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* MultiListFragment.kt
|
* MultiListFragment.kt
|
||||||
* Copyright (C) 2009-2021 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -96,9 +96,11 @@ abstract class MultiListFragment<T : Identifiable> : ScopeFragment(), Refreshabl
|
|||||||
if (title == null) {
|
if (title == null) {
|
||||||
FragmentTitle.setTitle(
|
FragmentTitle.setTitle(
|
||||||
this,
|
this,
|
||||||
if (listModel.isOffline())
|
if (listModel.isOffline()) {
|
||||||
R.string.music_library_label_offline
|
R.string.music_library_label_offline
|
||||||
else R.string.music_library_label
|
} else {
|
||||||
|
R.string.music_library_label
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
FragmentTitle.setTitle(this, title)
|
FragmentTitle.setTitle(this, title)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* PlayerFragment.kt
|
* PlayerFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -8,7 +8,6 @@
|
|||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Color.argb
|
import android.graphics.Color.argb
|
||||||
import android.graphics.Point
|
import android.graphics.Point
|
||||||
@ -18,7 +17,6 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
@ -31,15 +29,12 @@ import android.view.WindowManager
|
|||||||
import android.view.animation.AnimationUtils
|
import android.view.animation.AnimationUtils
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.PopupMenu
|
import android.widget.PopupMenu
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import android.widget.ViewFlipper
|
import android.widget.ViewFlipper
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.annotation.ColorInt
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.MenuHost
|
import androidx.core.view.MenuHost
|
||||||
@ -105,6 +100,7 @@ import org.moire.ultrasonic.util.CommunicationError
|
|||||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
|
import org.moire.ultrasonic.util.Util.themeColor
|
||||||
import org.moire.ultrasonic.util.Util.toast
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
import org.moire.ultrasonic.util.toTrack
|
import org.moire.ultrasonic.util.toTrack
|
||||||
import org.moire.ultrasonic.view.AutoRepeatButton
|
import org.moire.ultrasonic.view.AutoRepeatButton
|
||||||
@ -125,7 +121,6 @@ class PlayerFragment :
|
|||||||
private var swipeDistance = 0
|
private var swipeDistance = 0
|
||||||
private var swipeVelocity = 0
|
private var swipeVelocity = 0
|
||||||
private var jukeboxAvailable = false
|
private var jukeboxAvailable = false
|
||||||
private var useFiveStarRating = false
|
|
||||||
private var isEqualizerAvailable = false
|
private var isEqualizerAvailable = false
|
||||||
|
|
||||||
// Detectors & Callbacks
|
// Detectors & Callbacks
|
||||||
@ -151,6 +146,7 @@ class PlayerFragment :
|
|||||||
private lateinit var fiveStar3ImageView: ImageView
|
private lateinit var fiveStar3ImageView: ImageView
|
||||||
private lateinit var fiveStar4ImageView: ImageView
|
private lateinit var fiveStar4ImageView: ImageView
|
||||||
private lateinit var fiveStar5ImageView: ImageView
|
private lateinit var fiveStar5ImageView: ImageView
|
||||||
|
private lateinit var heartRatingImageView: ImageView
|
||||||
private lateinit var playlistFlipper: ViewFlipper
|
private lateinit var playlistFlipper: ViewFlipper
|
||||||
private lateinit var emptyTextView: TextView
|
private lateinit var emptyTextView: TextView
|
||||||
private lateinit var emptyView: ConstraintLayout
|
private lateinit var emptyView: ConstraintLayout
|
||||||
@ -174,12 +170,18 @@ class PlayerFragment :
|
|||||||
private lateinit var repeatButton: MaterialButton
|
private lateinit var repeatButton: MaterialButton
|
||||||
private lateinit var progressBar: SeekBar
|
private lateinit var progressBar: SeekBar
|
||||||
private lateinit var progressIndicator: CircularProgressIndicator
|
private lateinit var progressIndicator: CircularProgressIndicator
|
||||||
private val hollowStar = R.drawable.star_hollow_outline
|
|
||||||
private val fullStar = R.drawable.star_full_outline
|
private val hollowStar = R.drawable.rating_star_hollow_layered
|
||||||
|
private val fullStar = R.drawable.rating_star_full_layered
|
||||||
|
private val hollowHeart = R.drawable.rating_heart_hollow_layered
|
||||||
|
private val fullHeart = R.drawable.rating_heart_full_layered
|
||||||
private lateinit var hollowStarDrawable: Drawable
|
private lateinit var hollowStarDrawable: Drawable
|
||||||
private lateinit var fullStarDrawable: Drawable
|
private lateinit var fullStarDrawable: Drawable
|
||||||
|
private lateinit var hollowHeartDrawable: Drawable
|
||||||
|
private lateinit var fullHeartDrawable: Drawable
|
||||||
|
|
||||||
private var _binding: CurrentPlayingBinding? = null
|
private var _binding: CurrentPlayingBinding? = null
|
||||||
|
|
||||||
// This property is only valid between onCreateView and
|
// This property is only valid between onCreateView and
|
||||||
// onDestroyView.
|
// onDestroyView.
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
@ -232,6 +234,7 @@ class PlayerFragment :
|
|||||||
fiveStar3ImageView = view.findViewById(R.id.song_five_star_3)
|
fiveStar3ImageView = view.findViewById(R.id.song_five_star_3)
|
||||||
fiveStar4ImageView = view.findViewById(R.id.song_five_star_4)
|
fiveStar4ImageView = view.findViewById(R.id.song_five_star_4)
|
||||||
fiveStar5ImageView = view.findViewById(R.id.song_five_star_5)
|
fiveStar5ImageView = view.findViewById(R.id.song_five_star_5)
|
||||||
|
heartRatingImageView = view.findViewById(R.id.song_rating_heart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("LongMethod")
|
@Suppress("LongMethod")
|
||||||
@ -264,7 +267,6 @@ class PlayerFragment :
|
|||||||
Lifecycle.State.RESUMED
|
Lifecycle.State.RESUMED
|
||||||
)
|
)
|
||||||
|
|
||||||
useFiveStarRating = Settings.useFiveStarRating
|
|
||||||
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100
|
swipeDistance = (width + height) * PERCENTAGE_OF_SCREEN_FOR_SWIPE / 100
|
||||||
swipeVelocity = swipeDistance
|
swipeVelocity = swipeDistance
|
||||||
gestureScanner = GestureDetector(context, this)
|
gestureScanner = GestureDetector(context, this)
|
||||||
@ -276,19 +278,26 @@ class PlayerFragment :
|
|||||||
updateShuffleButtonState(mediaPlayerManager.isShufflePlayEnabled)
|
updateShuffleButtonState(mediaPlayerManager.isShufflePlayEnabled)
|
||||||
updateRepeatButtonState(mediaPlayerManager.repeatMode)
|
updateRepeatButtonState(mediaPlayerManager.repeatMode)
|
||||||
|
|
||||||
val ratingLinearLayout = view.findViewById<LinearLayout>(R.id.song_rating)
|
|
||||||
if (!useFiveStarRating) ratingLinearLayout.isVisible = false
|
|
||||||
|
|
||||||
hollowStarDrawable = ResourcesCompat.getDrawable(resources, hollowStar, null)!!
|
hollowStarDrawable = ResourcesCompat.getDrawable(resources, hollowStar, null)!!
|
||||||
fullStarDrawable = ResourcesCompat.getDrawable(resources, fullStar, null)!!
|
fullStarDrawable = ResourcesCompat.getDrawable(resources, fullStar, null)!!
|
||||||
setLayerDrawableColors(hollowStarDrawable as LayerDrawable)
|
setLayerDrawableColors(hollowStarDrawable as LayerDrawable)
|
||||||
setLayerDrawableColors(fullStarDrawable as LayerDrawable)
|
setLayerDrawableColors(fullStarDrawable as LayerDrawable)
|
||||||
|
|
||||||
|
hollowHeartDrawable = ResourcesCompat.getDrawable(resources, hollowHeart, null)!!
|
||||||
|
fullHeartDrawable = ResourcesCompat.getDrawable(resources, fullHeart, null)!!
|
||||||
|
setLayerDrawableColors(hollowHeartDrawable as LayerDrawable)
|
||||||
|
setLayerDrawableColors(
|
||||||
|
fullHeartDrawable as LayerDrawable,
|
||||||
|
RM.attr.colorAccent,
|
||||||
|
RM.attr.colorSurface
|
||||||
|
)
|
||||||
|
|
||||||
fiveStar1ImageView.setOnClickListener { setSongRating(1) }
|
fiveStar1ImageView.setOnClickListener { setSongRating(1) }
|
||||||
fiveStar2ImageView.setOnClickListener { setSongRating(2) }
|
fiveStar2ImageView.setOnClickListener { setSongRating(2) }
|
||||||
fiveStar3ImageView.setOnClickListener { setSongRating(3) }
|
fiveStar3ImageView.setOnClickListener { setSongRating(3) }
|
||||||
fiveStar4ImageView.setOnClickListener { setSongRating(4) }
|
fiveStar4ImageView.setOnClickListener { setSongRating(4) }
|
||||||
fiveStar5ImageView.setOnClickListener { setSongRating(5) }
|
fiveStar5ImageView.setOnClickListener { setSongRating(5) }
|
||||||
|
heartRatingImageView.setOnClickListener { setSongHeartRating() }
|
||||||
|
|
||||||
albumArtImageView.setOnTouchListener { _, me ->
|
albumArtImageView.setOnTouchListener { _, me ->
|
||||||
gestureScanner.onTouchEvent(me)
|
gestureScanner.onTouchEvent(me)
|
||||||
@ -333,8 +342,9 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
playButton.setOnClickListener {
|
playButton.setOnClickListener {
|
||||||
if (!mediaPlayerManager.isJukeboxEnabled)
|
if (!mediaPlayerManager.isJukeboxEnabled) {
|
||||||
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
networkAndStorageChecker.warnIfNetworkOrStorageUnavailable()
|
||||||
|
}
|
||||||
|
|
||||||
launch(CommunicationError.getHandler(context)) {
|
launch(CommunicationError.getHandler(context)) {
|
||||||
mediaPlayerManager.play()
|
mediaPlayerManager.play()
|
||||||
@ -407,6 +417,21 @@ class PlayerFragment :
|
|||||||
updateButtonStates(it.state)
|
updateButtonStates(it.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 == false) {
|
||||||
|
Toast.makeText(context, "Setting rating failed", Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
updateSongRatingDisplay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Query the Jukebox state in an IO Context
|
// Query the Jukebox state in an IO Context
|
||||||
ioScope.launch(CommunicationError.getHandler(context)) {
|
ioScope.launch(CommunicationError.getHandler(context)) {
|
||||||
try {
|
try {
|
||||||
@ -535,38 +560,13 @@ class PlayerFragment :
|
|||||||
val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer)
|
val equalizerMenuItem = menu.findItem(R.id.menu_item_equalizer)
|
||||||
val shareMenuItem = menu.findItem(R.id.menu_item_share)
|
val shareMenuItem = menu.findItem(R.id.menu_item_share)
|
||||||
val shareSongMenuItem = menu.findItem(R.id.menu_item_share_song)
|
val shareSongMenuItem = menu.findItem(R.id.menu_item_share_song)
|
||||||
val starMenuItem = menu.findItem(R.id.menu_item_star)
|
|
||||||
val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set)
|
val bookmarkMenuItem = menu.findItem(R.id.menu_item_bookmark_set)
|
||||||
val bookmarkRemoveMenuItem = menu.findItem(R.id.menu_item_bookmark_delete)
|
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)
|
|
||||||
starMenuItem.setTitle(R.string.download_menu_unstar)
|
|
||||||
} else {
|
|
||||||
starMenuItem.setIcon(hollowStar)
|
|
||||||
starMenuItem.setTitle(R.string.download_menu_star)
|
|
||||||
}
|
|
||||||
} else if (update.success == false) {
|
|
||||||
Toast.makeText(context, "Setting rating failed", Toast.LENGTH_SHORT)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOffline()) {
|
if (isOffline()) {
|
||||||
if (shareMenuItem != null) {
|
if (shareMenuItem != null) {
|
||||||
shareMenuItem.isVisible = false
|
shareMenuItem.isVisible = false
|
||||||
}
|
}
|
||||||
starMenuItem.isVisible = false
|
|
||||||
if (bookmarkMenuItem != null) {
|
if (bookmarkMenuItem != null) {
|
||||||
bookmarkMenuItem.isVisible = false
|
bookmarkMenuItem.isVisible = false
|
||||||
}
|
}
|
||||||
@ -585,15 +585,11 @@ class PlayerFragment :
|
|||||||
currentSong = track
|
currentSong = track
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useFiveStarRating) starMenuItem.isVisible = false
|
|
||||||
|
|
||||||
if (currentSong != null) {
|
if (currentSong != null) {
|
||||||
starMenuItem.setIcon(if (currentSong!!.starred) fullStar else hollowStar)
|
|
||||||
shareSongMenuItem.isVisible = true
|
shareSongMenuItem.isVisible = true
|
||||||
goToAlbum.isVisible = true
|
goToAlbum.isVisible = true
|
||||||
goToArtist.isVisible = true
|
goToArtist.isVisible = true
|
||||||
} else {
|
} else {
|
||||||
starMenuItem.setIcon(hollowStar)
|
|
||||||
shareSongMenuItem.isVisible = false
|
shareSongMenuItem.isVisible = false
|
||||||
goToAlbum.isVisible = false
|
goToAlbum.isVisible = false
|
||||||
goToArtist.isVisible = false
|
goToArtist.isVisible = false
|
||||||
@ -637,10 +633,7 @@ class PlayerFragment :
|
|||||||
return popup
|
return popup
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onContextMenuItemSelected(
|
private fun onContextMenuItemSelected(menuItem: MenuItem, item: MusicDirectory.Child): Boolean {
|
||||||
menuItem: MenuItem,
|
|
||||||
item: MusicDirectory.Child
|
|
||||||
): Boolean {
|
|
||||||
if (item !is Track) return false
|
if (item !is Track) return false
|
||||||
return menuItemSelected(menuItem.itemId, item)
|
return menuItemSelected(menuItem.itemId, item)
|
||||||
}
|
}
|
||||||
@ -707,8 +700,11 @@ class PlayerFragment :
|
|||||||
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
val jukeboxEnabled = !mediaPlayerManager.isJukeboxEnabled
|
||||||
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
mediaPlayerManager.isJukeboxEnabled = jukeboxEnabled
|
||||||
toast(
|
toast(
|
||||||
if (jukeboxEnabled) R.string.download_jukebox_on
|
if (jukeboxEnabled) {
|
||||||
else R.string.download_jukebox_off,
|
R.string.download_jukebox_on
|
||||||
|
} else {
|
||||||
|
R.string.download_jukebox_off
|
||||||
|
},
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
@ -729,16 +725,6 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.menu_item_star -> {
|
|
||||||
if (track == null) return true
|
|
||||||
track.starred = !track.starred
|
|
||||||
|
|
||||||
RxBus.ratingSubmitter.onNext(
|
|
||||||
RatingUpdate(track.id, HeartRating(track.starred))
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
R.id.menu_item_bookmark_set -> {
|
R.id.menu_item_bookmark_set -> {
|
||||||
if (track == null) return true
|
if (track == null) return true
|
||||||
|
|
||||||
@ -783,7 +769,7 @@ class PlayerFragment :
|
|||||||
}
|
}
|
||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this,
|
this,
|
||||||
tracks = tracks,
|
tracks = tracks
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -792,7 +778,7 @@ class PlayerFragment :
|
|||||||
|
|
||||||
shareHandler.createShare(
|
shareHandler.createShare(
|
||||||
this,
|
this,
|
||||||
listOf(track),
|
listOf(track)
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -876,7 +862,7 @@ class PlayerFragment :
|
|||||||
onContextMenuClick = { menu, id -> onContextMenuItemSelected(menu, id) },
|
onContextMenuClick = { menu, id -> onContextMenuItemSelected(menu, id) },
|
||||||
checkable = false,
|
checkable = false,
|
||||||
draggable = true,
|
draggable = true,
|
||||||
lifecycleOwner = viewLifecycleOwner,
|
lifecycleOwner = viewLifecycleOwner
|
||||||
) { view, track -> onCreateContextMenu(view, track) }.apply {
|
) { view, track -> onCreateContextMenu(view, track) }.apply {
|
||||||
this.startDrag = { holder ->
|
this.startDrag = { holder ->
|
||||||
dragTouchHelper.startDrag(holder)
|
dragTouchHelper.startDrag(holder)
|
||||||
@ -898,7 +884,6 @@ class PlayerFragment :
|
|||||||
viewHolder: RecyclerView.ViewHolder,
|
viewHolder: RecyclerView.ViewHolder,
|
||||||
target: RecyclerView.ViewHolder
|
target: RecyclerView.ViewHolder
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
||||||
val from = viewHolder.bindingAdapterPosition
|
val from = viewHolder.bindingAdapterPosition
|
||||||
val to = target.bindingAdapterPosition
|
val to = target.bindingAdapterPosition
|
||||||
|
|
||||||
@ -951,10 +936,7 @@ class PlayerFragment :
|
|||||||
mediaPlayerManager.removeFromPlaylist(pos)
|
mediaPlayerManager.removeFromPlaylist(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectedChanged(
|
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||||
viewHolder: RecyclerView.ViewHolder?,
|
|
||||||
actionState: Int
|
|
||||||
) {
|
|
||||||
super.onSelectedChanged(viewHolder, actionState)
|
super.onSelectedChanged(viewHolder, actionState)
|
||||||
|
|
||||||
if (actionState == ACTION_STATE_DRAG) {
|
if (actionState == ACTION_STATE_DRAG) {
|
||||||
@ -1009,8 +991,10 @@ class PlayerFragment :
|
|||||||
|
|
||||||
if (dX > 0) {
|
if (dX > 0) {
|
||||||
canvas.clipRect(
|
canvas.clipRect(
|
||||||
itemView.left.toFloat(), itemView.top.toFloat(),
|
itemView.left.toFloat(),
|
||||||
dX, itemView.bottom.toFloat()
|
itemView.top.toFloat(),
|
||||||
|
dX,
|
||||||
|
itemView.bottom.toFloat()
|
||||||
)
|
)
|
||||||
canvas.drawColor(backgroundColor)
|
canvas.drawColor(backgroundColor)
|
||||||
val left = itemView.left + Util.dpToPx(16, activity!!)
|
val left = itemView.left + Util.dpToPx(16, activity!!)
|
||||||
@ -1019,8 +1003,10 @@ class PlayerFragment :
|
|||||||
drawable?.draw(canvas)
|
drawable?.draw(canvas)
|
||||||
} else {
|
} else {
|
||||||
canvas.clipRect(
|
canvas.clipRect(
|
||||||
itemView.right.toFloat() + dX, itemView.top.toFloat(),
|
itemView.right.toFloat() + dX,
|
||||||
itemView.right.toFloat(), itemView.bottom.toFloat(),
|
itemView.top.toFloat(),
|
||||||
|
itemView.right.toFloat(),
|
||||||
|
itemView.bottom.toFloat()
|
||||||
)
|
)
|
||||||
canvas.drawColor(backgroundColor)
|
canvas.drawColor(backgroundColor)
|
||||||
val left = itemView.right - Util.dpToPx(16, activity!!) - iconSize
|
val left = itemView.right - Util.dpToPx(16, activity!!) - iconSize
|
||||||
@ -1034,7 +1020,13 @@ class PlayerFragment :
|
|||||||
viewHolder.itemView.translationX = dX
|
viewHolder.itemView.translationX = dX
|
||||||
} else {
|
} else {
|
||||||
super.onChildDraw(
|
super.onChildDraw(
|
||||||
canvas, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive
|
canvas,
|
||||||
|
recyclerView,
|
||||||
|
viewHolder,
|
||||||
|
dX,
|
||||||
|
dY,
|
||||||
|
actionState,
|
||||||
|
isCurrentlyActive
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1070,8 +1062,9 @@ class PlayerFragment :
|
|||||||
songTitleTextView.text = currentSong!!.title
|
songTitleTextView.text = currentSong!!.title
|
||||||
artistTextView.text = currentSong!!.artist
|
artistTextView.text = currentSong!!.artist
|
||||||
albumTextView.text = currentSong!!.album
|
albumTextView.text = currentSong!!.album
|
||||||
if (currentSong!!.year != null && Settings.showNowPlayingDetails)
|
if (currentSong!!.year != null && Settings.showNowPlayingDetails) {
|
||||||
albumTextView.append(String.format(Locale.ROOT, " (%d)", currentSong!!.year))
|
albumTextView.append(String.format(Locale.ROOT, " (%d)", currentSong!!.year))
|
||||||
|
}
|
||||||
|
|
||||||
if (Settings.showNowPlayingDetails) {
|
if (Settings.showNowPlayingDetails) {
|
||||||
genreTextView.text = currentSong!!.genre
|
genreTextView.text = currentSong!!.genre
|
||||||
@ -1079,11 +1072,12 @@ class PlayerFragment :
|
|||||||
(currentSong!!.genre != null && currentSong!!.genre!!.isNotBlank())
|
(currentSong!!.genre != null && currentSong!!.genre!!.isNotBlank())
|
||||||
|
|
||||||
var bitRate = ""
|
var bitRate = ""
|
||||||
if (currentSong!!.bitRate != null && currentSong!!.bitRate!! > 0)
|
if (currentSong!!.bitRate != null && currentSong!!.bitRate!! > 0) {
|
||||||
bitRate = String.format(
|
bitRate = String.format(
|
||||||
Util.appContext().getString(R.string.song_details_kbps),
|
Util.appContext().getString(R.string.song_details_kbps),
|
||||||
currentSong!!.bitRate
|
currentSong!!.bitRate
|
||||||
)
|
)
|
||||||
|
}
|
||||||
bitrateFormatTextView.text = String.format(
|
bitrateFormatTextView.text = String.format(
|
||||||
Locale.ROOT, "%s %s",
|
Locale.ROOT, "%s %s",
|
||||||
bitRate, currentSong!!.suffix
|
bitRate, currentSong!!.suffix
|
||||||
@ -1278,30 +1272,36 @@ class PlayerFragment :
|
|||||||
|
|
||||||
private fun updateSongRatingDisplay() {
|
private fun updateSongRatingDisplay() {
|
||||||
val rating = currentSong?.userRating ?: 0
|
val rating = currentSong?.userRating ?: 0
|
||||||
|
val isHeartSet = currentSong?.starred ?: false
|
||||||
|
|
||||||
fiveStar1ImageView.setImageDrawable(getStarForRating(rating, 0))
|
fiveStar1ImageView.setImageDrawable(getStarForRating(rating, 0))
|
||||||
fiveStar2ImageView.setImageDrawable(getStarForRating(rating, 1))
|
fiveStar2ImageView.setImageDrawable(getStarForRating(rating, 1))
|
||||||
fiveStar3ImageView.setImageDrawable(getStarForRating(rating, 2))
|
fiveStar3ImageView.setImageDrawable(getStarForRating(rating, 2))
|
||||||
fiveStar4ImageView.setImageDrawable(getStarForRating(rating, 3))
|
fiveStar4ImageView.setImageDrawable(getStarForRating(rating, 3))
|
||||||
fiveStar5ImageView.setImageDrawable(getStarForRating(rating, 4))
|
fiveStar5ImageView.setImageDrawable(getStarForRating(rating, 4))
|
||||||
|
|
||||||
|
if (isHeartSet) {
|
||||||
|
heartRatingImageView.setImageDrawable(fullHeartDrawable)
|
||||||
|
} else {
|
||||||
|
heartRatingImageView.setImageDrawable(hollowHeartDrawable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getStarForRating(rating: Int, position: Int): Drawable {
|
private fun getStarForRating(rating: Int, position: Int): Drawable {
|
||||||
return if (rating > position) fullStarDrawable else hollowStarDrawable
|
return if (rating > position) fullStarDrawable else hollowStarDrawable
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLayerDrawableColors(drawable: LayerDrawable) {
|
private fun setLayerDrawableColors(
|
||||||
|
drawable: LayerDrawable,
|
||||||
|
innerColor: Int = RM.attr.colorSurface,
|
||||||
|
borderColor: Int = RM.attr.colorAccent
|
||||||
|
) {
|
||||||
drawable.apply {
|
drawable.apply {
|
||||||
getDrawable(0).setTint(requireContext().themeColor(RM.attr.colorSurface))
|
getDrawable(0).setTint(requireContext().themeColor(innerColor))
|
||||||
getDrawable(1).setTint(requireContext().themeColor(RM.attr.colorAccent))
|
getDrawable(1).setTint(requireContext().themeColor(borderColor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
fun Context.themeColor(@AttrRes attrRes: Int): Int = TypedValue()
|
|
||||||
.apply { theme.resolveAttribute(attrRes, this, true) }
|
|
||||||
.data
|
|
||||||
|
|
||||||
private fun setSongRating(rating: Int) {
|
private fun setSongRating(rating: Int) {
|
||||||
if (currentSong == null) return
|
if (currentSong == null) return
|
||||||
currentSong?.userRating = rating
|
currentSong?.userRating = rating
|
||||||
@ -1315,6 +1315,19 @@ class PlayerFragment :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setSongHeartRating() {
|
||||||
|
if (currentSong == null) return
|
||||||
|
currentSong?.starred = !(currentSong?.starred ?: true)
|
||||||
|
updateSongRatingDisplay()
|
||||||
|
|
||||||
|
RxBus.ratingSubmitter.onNext(
|
||||||
|
RatingUpdate(
|
||||||
|
currentSong!!.id,
|
||||||
|
HeartRating(currentSong?.starred ?: false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
private fun showSavePlaylistDialog() {
|
private fun showSavePlaylistDialog() {
|
||||||
val layout = LayoutInflater.from(this.context)
|
val layout = LayoutInflater.from(this.context)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* SearchFragment.kt
|
* SearchFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -41,7 +41,6 @@ import org.moire.ultrasonic.subsonic.VideoPlayer.Companion.playVideo
|
|||||||
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenu
|
||||||
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
|
import org.moire.ultrasonic.util.ContextMenuUtil.handleContextMenuTracks
|
||||||
import org.moire.ultrasonic.util.RefreshableFragment
|
import org.moire.ultrasonic.util.RefreshableFragment
|
||||||
import org.moire.ultrasonic.util.Settings
|
|
||||||
import org.moire.ultrasonic.util.Util
|
import org.moire.ultrasonic.util.Util
|
||||||
import org.moire.ultrasonic.util.Util.toast
|
import org.moire.ultrasonic.util.Util.toast
|
||||||
import org.moire.ultrasonic.util.toastingExceptionHandler
|
import org.moire.ultrasonic.util.toastingExceptionHandler
|
||||||
@ -143,7 +142,6 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent, Re
|
|||||||
|
|
||||||
val artists = result.artists
|
val artists = result.artists
|
||||||
if (artists.isNotEmpty()) {
|
if (artists.isNotEmpty()) {
|
||||||
|
|
||||||
list.add(DividerBinder.Divider(R.string.search_artists))
|
list.add(DividerBinder.Divider(R.string.search_artists))
|
||||||
list.addAll(artists)
|
list.addAll(artists)
|
||||||
if (searchResult!!.artists.size > artists.size) {
|
if (searchResult!!.artists.size > artists.size) {
|
||||||
@ -283,10 +281,4 @@ class SearchFragment : MultiListFragment<Identifiable>(), KoinScopeComponent, Re
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
var DEFAULT_ARTISTS = Settings.defaultArtists
|
|
||||||
var DEFAULT_ALBUMS = Settings.defaultAlbums
|
|
||||||
var DEFAULT_SONGS = Settings.defaultSongs
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* SettingsFragment.kt
|
||||||
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.fragment
|
package org.moire.ultrasonic.fragment
|
||||||
|
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.SearchRecentSuggestions
|
import android.provider.SearchRecentSuggestions
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
@ -17,7 +24,6 @@ import androidx.preference.PreferenceFragmentCompat
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
|
||||||
import org.moire.ultrasonic.R
|
import org.moire.ultrasonic.R
|
||||||
import org.moire.ultrasonic.app.UApp
|
import org.moire.ultrasonic.app.UApp
|
||||||
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
import org.moire.ultrasonic.fragment.FragmentTitle.setTitle
|
||||||
@ -28,7 +34,7 @@ import org.moire.ultrasonic.log.FileLoggerTree.Companion.getLogFileSizes
|
|||||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest
|
import org.moire.ultrasonic.log.FileLoggerTree.Companion.plantToTimberForest
|
||||||
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
|
import org.moire.ultrasonic.log.FileLoggerTree.Companion.uprootFromTimberForest
|
||||||
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
import org.moire.ultrasonic.provider.SearchSuggestionProvider
|
||||||
import org.moire.ultrasonic.service.MediaPlayerManager
|
import org.moire.ultrasonic.service.DownloadService
|
||||||
import org.moire.ultrasonic.service.RxBus
|
import org.moire.ultrasonic.service.RxBus
|
||||||
import org.moire.ultrasonic.util.ConfirmationDialog
|
import org.moire.ultrasonic.util.ConfirmationDialog
|
||||||
import org.moire.ultrasonic.util.Constants
|
import org.moire.ultrasonic.util.Constants
|
||||||
@ -62,8 +68,6 @@ class SettingsFragment :
|
|||||||
private var debugLogToFile: CheckBoxPreference? = null
|
private var debugLogToFile: CheckBoxPreference? = null
|
||||||
private var customCacheLocation: CheckBoxPreference? = null
|
private var customCacheLocation: CheckBoxPreference? = null
|
||||||
|
|
||||||
private val mediaPlayerManager: MediaPlayerManager by inject()
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||||
}
|
}
|
||||||
@ -190,7 +194,7 @@ class SettingsFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheLocation?.isVisible = true
|
cacheLocation?.isVisible = true
|
||||||
val uri = Uri.parse(Settings.cacheLocationUri)
|
val uri = Settings.cacheLocationUri.toUri()
|
||||||
cacheLocation!!.summary = uri.path
|
cacheLocation!!.summary = uri.path
|
||||||
cacheLocation!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
cacheLocation!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
selectCacheLocation()
|
selectCacheLocation()
|
||||||
@ -261,7 +265,8 @@ class SettingsFragment :
|
|||||||
val choice = intArrayOf(defaultChoice)
|
val choice = intArrayOf(defaultChoice)
|
||||||
ConfirmationDialog.Builder(requireContext()).setTitle(title)
|
ConfirmationDialog.Builder(requireContext()).setTitle(title)
|
||||||
.setSingleChoiceItems(
|
.setSingleChoiceItems(
|
||||||
R.array.bluetoothDeviceSettingNames, defaultChoice
|
R.array.bluetoothDeviceSettingNames,
|
||||||
|
defaultChoice
|
||||||
) { _: DialogInterface?, i: Int -> choice[0] = i }
|
) { _: DialogInterface?, i: Int -> choice[0] = i }
|
||||||
.setNegativeButton(R.string.common_cancel) { dialogInterface: DialogInterface, _: Int ->
|
.setNegativeButton(R.string.common_cancel) { dialogInterface: DialogInterface, _: Int ->
|
||||||
dialogInterface.cancel()
|
dialogInterface.cancel()
|
||||||
@ -337,14 +342,14 @@ class SettingsFragment :
|
|||||||
|
|
||||||
private fun setCacheLocation(path: String) {
|
private fun setCacheLocation(path: String) {
|
||||||
if (path != "") {
|
if (path != "") {
|
||||||
val uri = Uri.parse(path)
|
val uri = path.toUri()
|
||||||
cacheLocation!!.summary = uri.path ?: ""
|
cacheLocation!!.summary = uri.path ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings.cacheLocationUri = path
|
Settings.cacheLocationUri = path
|
||||||
|
|
||||||
// Clear download queue.
|
// Clear download queue.
|
||||||
mediaPlayerManager.clear()
|
DownloadService.clearDownloads()
|
||||||
Storage.reset()
|
Storage.reset()
|
||||||
Storage.checkForErrorsWithCustomRoot()
|
Storage.checkForErrorsWithCustomRoot()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* TrackCollectionFragment.kt
|
* TrackCollectionFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -365,9 +365,7 @@ open class TrackCollectionFragment(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun playSelectedOrAllTracks(
|
private fun playSelectedOrAllTracks(insertionMode: MediaPlayerManager.InsertionMode) {
|
||||||
insertionMode: MediaPlayerManager.InsertionMode
|
|
||||||
) {
|
|
||||||
mediaPlayerManager.playTracksAndToast(
|
mediaPlayerManager.playTracksAndToast(
|
||||||
fragment = this,
|
fragment = this,
|
||||||
insertionMode = insertionMode,
|
insertionMode = insertionMode,
|
||||||
@ -403,9 +401,7 @@ open class TrackCollectionFragment(
|
|||||||
listModel.calculateButtonState(selection, ::updateButtonState)
|
listModel.calculateButtonState(selection, ::updateButtonState)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateButtonState(
|
private fun updateButtonState(show: TrackCollectionModel.Companion.ButtonStates) {
|
||||||
show: TrackCollectionModel.Companion.ButtonStates,
|
|
||||||
) {
|
|
||||||
// We are coming back from unknown context
|
// We are coming back from unknown context
|
||||||
// and need to ensure Main Thread in order to manipulate the UI
|
// 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 is null, our view was disposed in the meantime
|
||||||
@ -484,10 +480,11 @@ open class TrackCollectionFragment(
|
|||||||
internal fun getSelectedTracks(): 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)) {
|
||||||
it
|
it
|
||||||
else
|
} else {
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,7 +583,6 @@ open class TrackCollectionFragment(
|
|||||||
menuItem: MenuItem,
|
menuItem: MenuItem,
|
||||||
item: MusicDirectory.Child
|
item: MusicDirectory.Child
|
||||||
): Boolean {
|
): Boolean {
|
||||||
|
|
||||||
val tracks = getClickedSong(item)
|
val tracks = getClickedSong(item)
|
||||||
|
|
||||||
return ContextMenuUtil.handleContextMenuTracks(
|
return ContextMenuUtil.handleContextMenuTracks(
|
||||||
@ -600,10 +596,11 @@ open class TrackCollectionFragment(
|
|||||||
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
private fun getClickedSong(item: MusicDirectory.Child): List<Track> {
|
||||||
// This can probably be done better
|
// This can probably be done better
|
||||||
return viewAdapter.getCurrentList().mapNotNull {
|
return viewAdapter.getCurrentList().mapNotNull {
|
||||||
if (it is Track && (it.id == item.id))
|
if (it is Track && (it.id == item.id)) {
|
||||||
it
|
it
|
||||||
else
|
} else {
|
||||||
null
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,9 +112,10 @@ class ChatFragment : Fragment(), KoinComponent, RefreshableFragment {
|
|||||||
})
|
})
|
||||||
messageEditText.setOnEditorActionListener(
|
messageEditText.setOnEditorActionListener(
|
||||||
OnEditorActionListener {
|
OnEditorActionListener {
|
||||||
_: TextView?,
|
_: TextView?,
|
||||||
actionId: Int,
|
actionId: Int,
|
||||||
event: KeyEvent ->
|
event: KeyEvent
|
||||||
|
->
|
||||||
if (actionId == EditorInfo.IME_ACTION_SEND ||
|
if (actionId == EditorInfo.IME_ACTION_SEND ||
|
||||||
(actionId == EditorInfo.IME_NULL && event.action == KeyEvent.ACTION_DOWN)
|
(actionId == EditorInfo.IME_NULL && event.action == KeyEvent.ACTION_DOWN)
|
||||||
) {
|
) {
|
||||||
@ -170,7 +171,8 @@ class ChatFragment : Fragment(), KoinComponent, RefreshableFragment {
|
|||||||
requireActivity().runOnUiThread { load() }
|
requireActivity().runOnUiThread { load() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshInterval.toLong(), refreshInterval.toLong()
|
refreshInterval.toLong(),
|
||||||
|
refreshInterval.toLong()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* LyricsFragment.kt
|
* LyricsFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* PlaylistsFragment.kt
|
* PlaylistsFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -87,7 +87,7 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
|
|||||||
id = id,
|
id = id,
|
||||||
playlistId = id,
|
playlistId = id,
|
||||||
name = name,
|
name = name,
|
||||||
playlistName = name,
|
playlistName = name
|
||||||
)
|
)
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
@ -120,10 +120,14 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
|
|||||||
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
override fun onCreateContextMenu(menu: ContextMenu, view: View, menuInfo: ContextMenuInfo?) {
|
||||||
super.onCreateContextMenu(menu, view, menuInfo)
|
super.onCreateContextMenu(menu, view, menuInfo)
|
||||||
val inflater = requireActivity().menuInflater
|
val inflater = requireActivity().menuInflater
|
||||||
if (isOffline()) inflater.inflate(
|
if (isOffline()) {
|
||||||
R.menu.select_playlist_context_offline,
|
inflater.inflate(
|
||||||
menu
|
R.menu.select_playlist_context_offline,
|
||||||
) else inflater.inflate(R.menu.select_playlist_context, menu)
|
menu
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
inflater.inflate(R.menu.select_playlist_context, menu)
|
||||||
|
}
|
||||||
val downloadMenuItem = menu.findItem(R.id.playlist_menu_download)
|
val downloadMenuItem = menu.findItem(R.id.playlist_menu_download)
|
||||||
if (downloadMenuItem != null) {
|
if (downloadMenuItem != null) {
|
||||||
downloadMenuItem.isVisible = !isOffline()
|
downloadMenuItem.isVisible = !isOffline()
|
||||||
@ -236,13 +240,17 @@ class PlaylistsFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragme
|
|||||||
Comments: ${playlist.comment}
|
Comments: ${playlist.comment}
|
||||||
Song Count: ${playlist.songCount}
|
Song Count: ${playlist.songCount}
|
||||||
""".trimIndent() +
|
""".trimIndent() +
|
||||||
if (playlist.public == null) "" else """
|
if (playlist.public == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Public: ${playlist.public}
|
Public: ${playlist.public}
|
||||||
""".trimIndent() + """
|
""".trimIndent() + """
|
||||||
|
|
||||||
Creation Date: ${playlist.created.replace('T', ' ')}
|
Creation Date: ${playlist.created.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||||
textView.text = message
|
textView.text = message
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* PodcastFragment.kt
|
* PodcastFragment.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* SelectGenreFragment.kt
|
||||||
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.fragment.legacy
|
package org.moire.ultrasonic.fragment.legacy
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -53,8 +60,8 @@ class SelectGenreFragment : Fragment(), RefreshableFragment {
|
|||||||
swipeRefresh?.setOnRefreshListener { load(true) }
|
swipeRefresh?.setOnRefreshListener { load(true) }
|
||||||
|
|
||||||
genreListView?.setOnItemClickListener {
|
genreListView?.setOnItemClickListener {
|
||||||
parent: AdapterView<*>, _: View?,
|
parent: AdapterView<*>, _: View?,
|
||||||
position: Int, _: Long
|
position: Int, _: Long
|
||||||
->
|
->
|
||||||
val genre = parent.getItemAtPosition(position) as Genre
|
val genre = parent.getItemAtPosition(position) as Genre
|
||||||
|
|
||||||
|
@ -80,8 +80,9 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
swipeRefresh!!.setOnRefreshListener { load(true) }
|
swipeRefresh!!.setOnRefreshListener { load(true) }
|
||||||
emptyTextView = view.findViewById(R.id.select_share_empty)
|
emptyTextView = view.findViewById(R.id.select_share_empty)
|
||||||
sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
|
sharesListView!!.onItemClickListener = AdapterView.OnItemClickListener {
|
||||||
parent, _,
|
parent, _,
|
||||||
position, _ ->
|
position, _
|
||||||
|
->
|
||||||
val share = parent.getItemAtPosition(position) as Share
|
val share = parent.getItemAtPosition(position) as Share
|
||||||
|
|
||||||
val action = NavigationGraphDirections.toTrackCollection(
|
val action = NavigationGraphDirections.toTrackCollection(
|
||||||
@ -171,7 +172,7 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
insertionMode = MediaPlayerManager.InsertionMode.CLEAR,
|
||||||
id = share.id,
|
id = share.id,
|
||||||
name = share.name,
|
name = share.name,
|
||||||
shuffle = true,
|
shuffle = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
R.id.share_menu_delete -> {
|
R.id.share_menu_delete -> {
|
||||||
@ -235,21 +236,33 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
Visit Count: ${share.visitCount}
|
Visit Count: ${share.visitCount}
|
||||||
""".trimIndent() +
|
""".trimIndent() +
|
||||||
(
|
(
|
||||||
if (share.created == null) "" else """
|
if (share.created == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Creation Date: ${share.created!!.replace('T', ' ')}
|
Creation Date: ${share.created!!.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
) +
|
) +
|
||||||
(
|
(
|
||||||
if (share.lastVisited == null) "" else """
|
if (share.lastVisited == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
|
Last Visited Date: ${share.lastVisited!!.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
) +
|
) +
|
||||||
if (share.expires == null) "" else """
|
if (share.expires == null) {
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"""
|
||||||
|
|
||||||
Expiration Date: ${share.expires!!.replace('T', ' ')}
|
Expiration Date: ${share.expires!!.replace('T', ' ')}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
Linkify.addLinks(message, Linkify.WEB_URLS)
|
Linkify.addLinks(message, Linkify.WEB_URLS)
|
||||||
textView.text = message
|
textView.text = message
|
||||||
@ -289,11 +302,7 @@ class SharesFragment : ScopeFragment(), KoinScopeComponent, RefreshableFragment
|
|||||||
alertDialog.show()
|
alertDialog.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateShareOnServer(
|
private fun updateShareOnServer(millis: Long, description: String, share: Share) {
|
||||||
millis: Long,
|
|
||||||
description: String,
|
|
||||||
share: Share
|
|
||||||
) {
|
|
||||||
launchWithToast {
|
launchWithToast {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
val musicService = MusicServiceFactory.getMusicService()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* ArtworkBitmapLoader.kt
|
* ArtworkBitmapLoader.kt
|
||||||
* Copyright (C) 2009-2022 Ultrasonic developers
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
*
|
*
|
||||||
* Distributed under terms of the GNU GPLv3 license.
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
*/
|
*/
|
||||||
@ -32,6 +32,11 @@ class ArtworkBitmapLoader : BitmapLoader, KoinComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun supportsMimeType(mimeType: String): Boolean {
|
||||||
|
// TODO: Implement?
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> {
|
override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> {
|
||||||
return executorService.submit<Bitmap> {
|
return executorService.submit<Bitmap> {
|
||||||
decode(
|
decode(
|
||||||
|
@ -2,7 +2,6 @@ package org.moire.ultrasonic.imageloader
|
|||||||
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.os.Build
|
|
||||||
import com.squareup.picasso.Picasso.LoadedFrom.DISK
|
import com.squareup.picasso.Picasso.LoadedFrom.DISK
|
||||||
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
|
import com.squareup.picasso.Picasso.LoadedFrom.NETWORK
|
||||||
import com.squareup.picasso.Request
|
import com.squareup.picasso.Request
|
||||||
@ -65,10 +64,7 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
|
|||||||
throw IOException("${response.apiError}")
|
throw IOException("${response.apiError}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAlbumArtBitmapFromDisk(
|
private fun getAlbumArtBitmapFromDisk(filename: String, size: Int?): Bitmap? {
|
||||||
filename: String,
|
|
||||||
size: Int?
|
|
||||||
): Bitmap? {
|
|
||||||
val albumArtFile = FileUtil.getAlbumArtFile(filename)
|
val albumArtFile = FileUtil.getAlbumArtFile(filename)
|
||||||
val bitmap: Bitmap? = null
|
val bitmap: Bitmap? = null
|
||||||
if (File(albumArtFile).exists()) {
|
if (File(albumArtFile).exists()) {
|
||||||
@ -77,11 +73,7 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getBitmapFromDisk(
|
private fun getBitmapFromDisk(path: String, size: Int?, bitmap: Bitmap?): Bitmap? {
|
||||||
path: String,
|
|
||||||
size: Int?,
|
|
||||||
bitmap: Bitmap?
|
|
||||||
): Bitmap? {
|
|
||||||
var bitmap1 = bitmap
|
var bitmap1 = bitmap
|
||||||
val opt = BitmapFactory.Options()
|
val opt = BitmapFactory.Options()
|
||||||
if (size != null && size > 0) {
|
if (size != null && size > 0) {
|
||||||
@ -92,12 +84,6 @@ class CoverArtRequestHandler(private val client: SubsonicAPIClient) : RequestHan
|
|||||||
BitmapFactory.decodeFile(path, opt)
|
BitmapFactory.decodeFile(path, opt)
|
||||||
|
|
||||||
// Now set the remaining flags
|
// Now set the remaining flags
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
||||||
opt.inDither = true
|
|
||||||
opt.inPreferQualityOverSpeed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
opt.inSampleSize = Util.calculateInSampleSize(
|
opt.inSampleSize = Util.calculateInSampleSize(
|
||||||
opt,
|
opt,
|
||||||
size,
|
size,
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* ImageLoader.kt
|
||||||
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.imageloader
|
package org.moire.ultrasonic.imageloader
|
||||||
|
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
@ -38,7 +45,7 @@ import timber.log.Timber
|
|||||||
class ImageLoader(
|
class ImageLoader(
|
||||||
context: Context,
|
context: Context,
|
||||||
apiClient: SubsonicAPIClient,
|
apiClient: SubsonicAPIClient,
|
||||||
private val config: ImageLoaderConfig,
|
private val config: ImageLoaderConfig
|
||||||
) : CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
|
) : CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
|
||||||
private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap()
|
private val cacheInProgress: ConcurrentHashMap<String, CountDownLatch> = ConcurrentHashMap()
|
||||||
|
|
||||||
@ -112,7 +119,10 @@ class ImageLoader(
|
|||||||
val requestedSize = resolveSize(size, large)
|
val requestedSize = resolveSize(size, large)
|
||||||
|
|
||||||
val request = ImageRequest.CoverArt(
|
val request = ImageRequest.CoverArt(
|
||||||
id!!, cacheKey!!, null, requestedSize,
|
id!!,
|
||||||
|
cacheKey!!,
|
||||||
|
null,
|
||||||
|
requestedSize,
|
||||||
placeHolderDrawableRes = defaultResourceId,
|
placeHolderDrawableRes = defaultResourceId,
|
||||||
errorDrawableRes = defaultResourceId
|
errorDrawableRes = defaultResourceId
|
||||||
)
|
)
|
||||||
@ -157,7 +167,10 @@ class ImageLoader(
|
|||||||
|
|
||||||
if (id != null && key != null && id.isNotEmpty() && view is ImageView) {
|
if (id != null && key != null && id.isNotEmpty() && view is ImageView) {
|
||||||
val request = ImageRequest.CoverArt(
|
val request = ImageRequest.CoverArt(
|
||||||
id, key, view, requestedSize,
|
id,
|
||||||
|
key,
|
||||||
|
view,
|
||||||
|
requestedSize,
|
||||||
placeHolderDrawableRes = defaultResourceId,
|
placeHolderDrawableRes = defaultResourceId,
|
||||||
errorDrawableRes = defaultResourceId
|
errorDrawableRes = defaultResourceId
|
||||||
)
|
)
|
||||||
@ -170,13 +183,11 @@ class ImageLoader(
|
|||||||
/**
|
/**
|
||||||
* Load the avatar of a given user into an ImageView
|
* Load the avatar of a given user into an ImageView
|
||||||
*/
|
*/
|
||||||
fun loadAvatarImage(
|
fun loadAvatarImage(view: ImageView, username: String) {
|
||||||
view: ImageView,
|
|
||||||
username: String
|
|
||||||
) {
|
|
||||||
if (username.isNotEmpty()) {
|
if (username.isNotEmpty()) {
|
||||||
val request = ImageRequest.Avatar(
|
val request = ImageRequest.Avatar(
|
||||||
username, view,
|
username,
|
||||||
|
view,
|
||||||
placeHolderDrawableRes = R.drawable.ic_contact_picture,
|
placeHolderDrawableRes = R.drawable.ic_contact_picture,
|
||||||
errorDrawableRes = R.drawable.ic_contact_picture
|
errorDrawableRes = R.drawable.ic_contact_picture
|
||||||
)
|
)
|
||||||
@ -284,7 +295,7 @@ sealed class ImageRequest(
|
|||||||
imageView: ImageView?,
|
imageView: ImageView?,
|
||||||
val size: Int,
|
val size: Int,
|
||||||
placeHolderDrawableRes: Int? = null,
|
placeHolderDrawableRes: Int? = null,
|
||||||
errorDrawableRes: Int? = null,
|
errorDrawableRes: Int? = null
|
||||||
) : ImageRequest(
|
) : ImageRequest(
|
||||||
placeHolderDrawableRes,
|
placeHolderDrawableRes,
|
||||||
errorDrawableRes,
|
errorDrawableRes,
|
||||||
|
@ -13,17 +13,15 @@ internal const val QUERY_USERNAME = "username"
|
|||||||
* Picasso.load() only accepts an URI as parameter. Therefore we create a bogus URI, in which
|
* Picasso.load() only accepts an URI as parameter. Therefore we create a bogus URI, in which
|
||||||
* we encode the data that we need in the RequestHandler.
|
* we encode the data that we need in the RequestHandler.
|
||||||
*/
|
*/
|
||||||
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri =
|
internal fun createLoadCoverArtRequest(entityId: String, size: Long? = 0): Uri = Uri.Builder()
|
||||||
Uri.Builder()
|
.scheme(SCHEME)
|
||||||
.scheme(SCHEME)
|
.appendPath(COVER_ART_PATH)
|
||||||
.appendPath(COVER_ART_PATH)
|
.appendQueryParameter(QUERY_ID, entityId)
|
||||||
.appendQueryParameter(QUERY_ID, entityId)
|
.appendQueryParameter(SIZE, size.toString())
|
||||||
.appendQueryParameter(SIZE, size.toString())
|
.build()
|
||||||
.build()
|
|
||||||
|
|
||||||
internal fun createLoadAvatarRequest(username: String): Uri =
|
internal fun createLoadAvatarRequest(username: String): Uri = Uri.Builder()
|
||||||
Uri.Builder()
|
.scheme(SCHEME)
|
||||||
.scheme(SCHEME)
|
.appendPath(AVATAR_PATH)
|
||||||
.appendPath(AVATAR_PATH)
|
.appendQueryParameter(QUERY_USERNAME, username)
|
||||||
.appendQueryParameter(QUERY_USERNAME, username)
|
.build()
|
||||||
.build()
|
|
||||||
|
@ -22,11 +22,7 @@ class AlbumListModel(application: Application) : GenericListModel(application) {
|
|||||||
private var lastType: AlbumListType? = null
|
private var lastType: AlbumListType? = null
|
||||||
private var loadedUntil: Int = 0
|
private var loadedUntil: Int = 0
|
||||||
|
|
||||||
suspend fun getAlbumsOfArtist(
|
suspend fun getAlbumsOfArtist(refresh: Boolean, id: String, name: String?) {
|
||||||
refresh: Boolean,
|
|
||||||
id: String,
|
|
||||||
name: String?
|
|
||||||
) {
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
list.postValue(service.getAlbumsOfArtist(id, name, refresh))
|
list.postValue(service.getAlbumsOfArtist(id, name, refresh))
|
||||||
|
@ -59,36 +59,33 @@ class EditServerModel(val app: Application) : AndroidViewModel(app), KoinCompone
|
|||||||
return (this.status === SubsonicResponse.Status.OK)
|
return (this.status === SubsonicResponse.Status.OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun requestFlow(
|
private fun requestFlow(type: ServerFeature, api: SubsonicAPIDefinition, userName: String) =
|
||||||
type: ServerFeature,
|
flow {
|
||||||
api: SubsonicAPIDefinition,
|
when (type) {
|
||||||
userName: String
|
ServerFeature.CHAT -> emit(
|
||||||
) = flow {
|
serverFunctionAvailable(type, api::getChatMessagesSuspend)
|
||||||
when (type) {
|
)
|
||||||
ServerFeature.CHAT -> emit(
|
ServerFeature.BOOKMARK -> emit(
|
||||||
serverFunctionAvailable(type, api::getChatMessagesSuspend)
|
serverFunctionAvailable(type, api::getBookmarksSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.BOOKMARK -> emit(
|
ServerFeature.SHARE -> emit(
|
||||||
serverFunctionAvailable(type, api::getBookmarksSuspend)
|
serverFunctionAvailable(type, api::getSharesSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.SHARE -> emit(
|
ServerFeature.PODCAST -> emit(
|
||||||
serverFunctionAvailable(type, api::getSharesSuspend)
|
serverFunctionAvailable(type, api::getPodcastsSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.PODCAST -> emit(
|
ServerFeature.JUKEBOX -> emit(
|
||||||
serverFunctionAvailable(type, api::getPodcastsSuspend)
|
serverFunctionAvailable(type) {
|
||||||
)
|
val response = api.getUserSuspend(userName)
|
||||||
ServerFeature.JUKEBOX -> emit(
|
if (!response.user.jukeboxRole) throw IOException()
|
||||||
serverFunctionAvailable(type) {
|
response
|
||||||
val response = api.getUserSuspend(userName)
|
}
|
||||||
if (!response.user.jukeboxRole) throw IOException()
|
)
|
||||||
response
|
ServerFeature.VIDEO -> emit(
|
||||||
}
|
serverFunctionAvailable(type, api::getVideosSuspend)
|
||||||
)
|
)
|
||||||
ServerFeature.VIDEO -> emit(
|
}
|
||||||
serverFunctionAvailable(type, api::getVideosSuspend)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
|
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
|
||||||
|
@ -67,10 +67,7 @@ open class GenericListModel(application: Application) :
|
|||||||
/**
|
/**
|
||||||
* Trigger a load() and notify the UI that we are loading
|
* Trigger a load() and notify the UI that we are loading
|
||||||
*/
|
*/
|
||||||
fun backgroundLoadFromServer(
|
fun backgroundLoadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
|
||||||
refresh: Boolean,
|
|
||||||
swipe: SwipeRefreshLayout
|
|
||||||
) {
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
swipe.isRefreshing = true
|
swipe.isRefreshing = true
|
||||||
loadFromServer(refresh, swipe)
|
loadFromServer(refresh, swipe)
|
||||||
@ -81,10 +78,7 @@ open class GenericListModel(application: Application) :
|
|||||||
/**
|
/**
|
||||||
* Calls the load() function with error handling
|
* Calls the load() function with error handling
|
||||||
*/
|
*/
|
||||||
private suspend fun loadFromServer(
|
private suspend fun loadFromServer(refresh: Boolean, swipe: SwipeRefreshLayout) {
|
||||||
refresh: Boolean,
|
|
||||||
swipe: SwipeRefreshLayout
|
|
||||||
) {
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val musicService = MusicServiceFactory.getMusicService()
|
val musicService = MusicServiceFactory.getMusicService()
|
||||||
val isOffline = ActiveServerProvider.isOffline()
|
val isOffline = ActiveServerProvider.isOffline()
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
/*
|
||||||
|
* SearchListModel.kt
|
||||||
|
* Copyright (C) 2009-2023 Ultrasonic developers
|
||||||
|
*
|
||||||
|
* Distributed under terms of the GNU GPLv3 license.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.moire.ultrasonic.model
|
package org.moire.ultrasonic.model
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
@ -6,7 +13,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
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.fragment.SearchFragment
|
|
||||||
import org.moire.ultrasonic.service.MusicServiceFactory
|
import org.moire.ultrasonic.service.MusicServiceFactory
|
||||||
import org.moire.ultrasonic.util.Settings
|
import org.moire.ultrasonic.util.Settings
|
||||||
|
|
||||||
@ -31,9 +37,9 @@ class SearchListModel(application: Application) : GenericListModel(application)
|
|||||||
|
|
||||||
fun trimResultLength(
|
fun trimResultLength(
|
||||||
result: SearchResult,
|
result: SearchResult,
|
||||||
maxArtists: Int = SearchFragment.DEFAULT_ARTISTS,
|
maxArtists: Int = Settings.defaultArtists,
|
||||||
maxAlbums: Int = SearchFragment.DEFAULT_ALBUMS,
|
maxAlbums: Int = Settings.defaultAlbums,
|
||||||
maxSongs: Int = SearchFragment.DEFAULT_SONGS
|
maxSongs: Int = Settings.defaultSongs
|
||||||
): SearchResult {
|
): SearchResult {
|
||||||
return SearchResult(
|
return SearchResult(
|
||||||
artists = result.artists.take(maxArtists),
|
artists = result.artists.take(maxArtists),
|
||||||
|
@ -30,13 +30,9 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
private var loadedUntil: Int = 0
|
private var loadedUntil: Int = 0
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
* Especially when dealing with indexes, this method can return Albums, Entries or a mix of both!
|
||||||
*/
|
*/
|
||||||
suspend fun getMusicDirectory(
|
suspend fun getMusicDirectory(refresh: Boolean, id: String, name: String?) {
|
||||||
refresh: Boolean,
|
|
||||||
id: String,
|
|
||||||
name: String?
|
|
||||||
) {
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = service.getMusicDirectory(id, name, refresh)
|
val musicDirectory = service.getMusicDirectory(id, name, refresh)
|
||||||
@ -46,9 +42,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAlbum(refresh: Boolean, id: String, name: String?) {
|
suspend fun getAlbum(refresh: Boolean, id: String, name: String?) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory: MusicDirectory = service.getAlbumAsDir(id, name, refresh)
|
val musicDirectory: MusicDirectory = service.getAlbumAsDir(id, name, refresh)
|
||||||
currentListIsSortable = true
|
currentListIsSortable = true
|
||||||
@ -74,9 +68,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getStarred() {
|
suspend fun getStarred() {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory: MusicDirectory
|
val musicDirectory: MusicDirectory
|
||||||
|
|
||||||
@ -122,7 +114,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getPodcastEpisodes(podcastChannelId: String) {
|
suspend fun getPodcastEpisodes(podcastChannelId: String) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = service.getPodcastEpisodes(podcastChannelId)
|
val musicDirectory = service.getPodcastEpisodes(podcastChannelId)
|
||||||
@ -134,7 +125,6 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getShare(shareId: String) {
|
suspend fun getShare(shareId: String) {
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val service = MusicServiceFactory.getMusicService()
|
val service = MusicServiceFactory.getMusicService()
|
||||||
val musicDirectory = MusicDirectory()
|
val musicDirectory = MusicDirectory()
|
||||||
@ -174,10 +164,7 @@ class TrackCollectionModel(application: Application) : GenericListModel(applicat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun calculateButtonState(
|
fun calculateButtonState(selection: List<Track>, onComplete: (ButtonStates) -> Unit) {
|
||||||
selection: List<Track>,
|
|
||||||
onComplete: (ButtonStates) -> Unit
|
|
||||||
) {
|
|
||||||
val enabled = selection.isNotEmpty()
|
val enabled = selection.isNotEmpty()
|
||||||
var unpinEnabled = false
|
var unpinEnabled = false
|
||||||
var deleteEnabled = false
|
var deleteEnabled = false
|
||||||
|
@ -38,7 +38,9 @@ class AlbumArtContentProvider : ContentProvider(), KoinComponent {
|
|||||||
.path(
|
.path(
|
||||||
String.format(
|
String.format(
|
||||||
Locale.ROOT,
|
Locale.ROOT,
|
||||||
"%s|%s", track!!.coverArt, FileUtil.getAlbumArtKey(track, true)
|
"%s|%s",
|
||||||
|
track!!.coverArt,
|
||||||
|
FileUtil.getAlbumArtKey(track, true)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
|
@ -13,7 +13,6 @@ import android.appwidget.AppWidgetProvider
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
@ -160,11 +159,7 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
|||||||
/**
|
/**
|
||||||
* Update Track details in widgets
|
* Update Track details in widgets
|
||||||
*/
|
*/
|
||||||
private fun updateTrack(
|
private fun updateTrack(context: Context, views: RemoteViews, currentSong: Track?) {
|
||||||
context: Context,
|
|
||||||
views: RemoteViews,
|
|
||||||
currentSong: Track?
|
|
||||||
) {
|
|
||||||
Timber.d("Updating Widget")
|
Timber.d("Updating Widget")
|
||||||
val res = context.resources
|
val res = context.resources
|
||||||
val title = currentSong?.title
|
val title = currentSong?.title
|
||||||
@ -222,10 +217,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
|||||||
intent.action = Intent.ACTION_MAIN
|
intent.action = Intent.ACTION_MAIN
|
||||||
intent.addCategory(Intent.CATEGORY_LAUNCHER)
|
intent.addCategory(Intent.CATEGORY_LAUNCHER)
|
||||||
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
var flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
// needed starting Android 12 (S = 31)
|
||||||
// needed starting Android 12 (S = 31)
|
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
||||||
flags = flags or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
}
|
|
||||||
var pendingIntent =
|
var pendingIntent =
|
||||||
PendingIntent.getActivity(context, 10, intent, flags)
|
PendingIntent.getActivity(context, 10, intent, flags)
|
||||||
views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent)
|
views.setOnClickPendingIntent(R.id.appwidget_coverart, pendingIntent)
|
||||||
@ -239,10 +232,8 @@ open class UltrasonicAppWidgetProvider : AppWidgetProvider() {
|
|||||||
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
|
KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)
|
||||||
)
|
)
|
||||||
flags = 0
|
flags = 0
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
// needed starting Android 12 (S = 31)
|
||||||
// needed starting Android 12 (S = 31)
|
flags = PendingIntent.FLAG_IMMUTABLE
|
||||||
flags = PendingIntent.FLAG_IMMUTABLE
|
|
||||||
}
|
|
||||||
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
|
pendingIntent = PendingIntent.getBroadcast(context, 11, intent, flags)
|
||||||
views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
|
views.setOnClickPendingIntent(R.id.control_play, pendingIntent)
|
||||||
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user