Merge branch 'ready/useFlow2' into 'develop'

Retrieve server features in parallel, retrieve Jukebox capabilities as server feature.

Closes #829

See merge request ultrasonic/ultrasonic!886
This commit is contained in:
Nite 2023-01-06 19:20:25 +00:00
commit ffdd5df82d
32 changed files with 455 additions and 232 deletions

View File

@ -1,7 +1,7 @@
<component name="InspectionProjectProfileManager"> <component name="InspectionProjectProfileManager">
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="Reformat" enabled="true" level="WEAK WARNING" enabled_by_default="true"> <inspection_tool class="Reformat" enabled="false" level="WEAK WARNING" enabled_by_default="false">
<option name="processChangedFilesOnly" value="true" /> <option name="processChangedFilesOnly" value="true" />
</inspection_tool> </inspection_tool>
</profile> </profile>

View File

@ -54,6 +54,9 @@ interface SubsonicAPIDefinition {
@GET("ping.view") @GET("ping.view")
fun ping(): Call<SubsonicResponse> fun ping(): Call<SubsonicResponse>
@GET("ping.view")
suspend fun pingSuspend(): SubsonicResponse
@GET("getLicense.view") @GET("getLicense.view")
fun getLicense(): Call<LicenseResponse> fun getLicense(): Call<LicenseResponse>
@ -164,6 +167,12 @@ interface SubsonicAPIDefinition {
@Query("id") id: String? = null @Query("id") id: String? = null
): Call<GetPodcastsResponse> ): Call<GetPodcastsResponse>
@GET("getPodcasts.view")
suspend fun getPodcastsSuspend(
@Query("includeEpisodes") includeEpisodes: Boolean? = null,
@Query("id") id: String? = null
): GetPodcastsResponse
@GET("getLyrics.view") @GET("getLyrics.view")
fun getLyrics( fun getLyrics(
@Query("artist") artist: String? = null, @Query("artist") artist: String? = null,
@ -261,6 +270,9 @@ interface SubsonicAPIDefinition {
@GET("getShares.view") @GET("getShares.view")
fun getShares(): Call<SharesResponse> fun getShares(): Call<SharesResponse>
@GET("getShares.view")
suspend fun getSharesSuspend(): SharesResponse
@GET("createShare.view") @GET("createShare.view")
fun createShare( fun createShare(
@Query("id") idsToShare: List<String>, @Query("id") idsToShare: List<String>,
@ -292,15 +304,24 @@ interface SubsonicAPIDefinition {
@GET("getUser.view") @GET("getUser.view")
fun getUser(@Query("username") username: String): Call<GetUserResponse> fun getUser(@Query("username") username: String): Call<GetUserResponse>
@GET("getUser.view")
suspend fun getUserSuspend(@Query("username") username: String): GetUserResponse
@GET("getChatMessages.view") @GET("getChatMessages.view")
fun getChatMessages(@Query("since") since: Long? = null): Call<ChatMessagesResponse> fun getChatMessages(@Query("since") since: Long? = null): Call<ChatMessagesResponse>
@GET("getChatMessages.view")
suspend fun getChatMessagesSuspend(@Query("since") since: Long? = null): ChatMessagesResponse
@GET("addChatMessage.view") @GET("addChatMessage.view")
fun addChatMessage(@Query("message") message: String): Call<SubsonicResponse> fun addChatMessage(@Query("message") message: String): Call<SubsonicResponse>
@GET("getBookmarks.view") @GET("getBookmarks.view")
fun getBookmarks(): Call<BookmarksResponse> fun getBookmarks(): Call<BookmarksResponse>
@GET("getBookmarks.view")
suspend fun getBookmarksSuspend(): BookmarksResponse
@GET("createBookmark.view") @GET("createBookmark.view")
fun createBookmark( fun createBookmark(
@Query("id") id: String, @Query("id") id: String,
@ -314,6 +335,9 @@ interface SubsonicAPIDefinition {
@GET("getVideos.view") @GET("getVideos.view")
fun getVideos(): Call<VideosResponse> fun getVideos(): Call<VideosResponse>
@GET("getVideos.view")
suspend fun getVideosSuspend(): VideosResponse
@GET("getAvatar.view") @GET("getAvatar.view")
fun getAvatar(@Query("username") username: String): Call<ResponseBody> fun getAvatar(@Query("username") username: String): Call<ResponseBody>
} }

View File

@ -66,6 +66,8 @@ style:
active: false active: false
ReturnCount: ReturnCount:
max: 5 max: 5
ForbiddenImport:
imports: ['android.app.AlertDialog']
comments: comments:
active: true active: true

View File

@ -0,0 +1,136 @@
{
"formatVersion": 1,
"database": {
"version": 6,
"identityHash": "9d28146ad3086d9c761f25ca007a96ce",
"entities": [
{
"tableName": "ServerSetting",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "index",
"columnName": "index",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "userName",
"columnName": "userName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "password",
"columnName": "password",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "jukeboxByDefault",
"columnName": "jukeboxByDefault",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "allowSelfSignedCertificate",
"columnName": "allowSelfSignedCertificate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forcePlainTextPassword",
"columnName": "forcePlainTextPassword",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "musicFolderId",
"columnName": "musicFolderId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "minimumApiVersion",
"columnName": "minimumApiVersion",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "chatSupport",
"columnName": "chatSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "bookmarkSupport",
"columnName": "bookmarkSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "shareSupport",
"columnName": "shareSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "podcastSupport",
"columnName": "podcastSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "jukeboxSupport",
"columnName": "jukeboxSupport",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "videoSupport",
"columnName": "videoSupport",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9d28146ad3086d9c761f25ca007a96ce')"
]
}
}

View File

@ -14,7 +14,6 @@ import android.view.inputmethod.EditorInfo;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ListAdapter; import android.widget.ListAdapter;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;

View File

@ -19,10 +19,8 @@
package org.moire.ultrasonic.util; package org.moire.ultrasonic.util;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import org.moire.ultrasonic.R; import org.moire.ultrasonic.R;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -47,9 +45,9 @@ public abstract class ModalBackgroundTask<T> extends BackgroundTask<T>
this(activity, true); this(activity, true);
} }
private AlertDialog createProgressDialog() private androidx.appcompat.app.AlertDialog createProgressDialog()
{ {
AlertDialog.Builder builder = new InfoDialog.Builder(getActivity()); InfoDialog.Builder builder = new InfoDialog.Builder(getActivity().getApplicationContext());
builder.setTitle(R.string.background_task_wait); builder.setTitle(R.string.background_task_wait);
builder.setMessage(R.string.background_task_loading); builder.setMessage(R.string.background_task_loading);
builder.setOnCancelListener(dialogInterface -> cancel()); builder.setOnCancelListener(dialogInterface -> cancel());

View File

@ -172,9 +172,6 @@ class NavigationActivity : AppCompatActivity() {
} else { } else {
if (!nowPlayingHidden) showNowPlaying() if (!nowPlayingHidden) showNowPlaying()
} }
// Hides menu items for Offline mode
setMenuForServerCapabilities()
} }
// Determine if this is a first run // Determine if this is a first run
@ -207,6 +204,7 @@ class NavigationActivity : AppCompatActivity() {
rxBusSubscription += RxBus.activeServerChangedObservable.subscribe { rxBusSubscription += RxBus.activeServerChangedObservable.subscribe {
updateNavigationHeaderForServer() updateNavigationHeaderForServer()
setMenuForServerCapabilities()
} }
serverRepository.liveServerCount().observe(this) { count -> serverRepository.liveServerCount().observe(this) { count ->
@ -506,6 +504,6 @@ class NavigationActivity : AppCompatActivity() {
podcastsMenuItem?.isVisible = activeServer.podcastSupport != false podcastsMenuItem?.isVisible = activeServer.podcastSupport != false
playlistsMenuItem?.isVisible = isOnline playlistsMenuItem?.isVisible = isOnline
downloadsMenuItem?.isVisible = isOnline downloadsMenuItem?.isVisible = isOnline
videoMenuItem?.isVisible = isOnline videoMenuItem?.isVisible = activeServer.videoSupport != false
} }
} }

View File

@ -214,7 +214,9 @@ class ActiveServerProvider(
bookmarkSupport = false, bookmarkSupport = false,
podcastSupport = false, podcastSupport = false,
shareSupport = false, shareSupport = false,
chatSupport = false chatSupport = false,
videoSupport = false,
jukeboxSupport = false
) )
/** /**

View File

@ -11,7 +11,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
*/ */
@Database( @Database(
entities = [ServerSetting::class], entities = [ServerSetting::class],
version = 5, version = 6,
exportSchema = true exportSchema = true
) )
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -265,7 +265,6 @@ val MIGRATION_5_4: Migration = object : Migration(5, 4) {
database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`") database.execSQL("ALTER TABLE `_new_ServerSetting` RENAME TO `ServerSetting`")
} }
} }
/* ktlint-disable max-line-length */ /* 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(database: SupportSQLiteDatabase) {

View File

@ -17,8 +17,6 @@ import androidx.room.PrimaryKey
* @param forcePlainTextPassword: True if the server authenticates the user using old Ldap-like way * @param forcePlainTextPassword: True if the server authenticates the user using old Ldap-like way
* @param musicFolderId: The Id of the MusicFolder to be used with the server * @param musicFolderId: The Id of the MusicFolder to be used with the server
* *
* TODO: forcePlainTextPassword is still using the old column name.
* Could be updated on the next significant change to the DB scheme
*/ */
@Entity @Entity
data class ServerSetting( data class ServerSetting(
@ -32,14 +30,17 @@ data class ServerSetting(
@ColumnInfo(name = "password") var password: String, @ColumnInfo(name = "password") var password: String,
@ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean, @ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean,
@ColumnInfo(name = "allowSelfSignedCertificate") var allowSelfSignedCertificate: Boolean, @ColumnInfo(name = "allowSelfSignedCertificate") var allowSelfSignedCertificate: Boolean,
@ColumnInfo(name = "ldapSupport") var forcePlainTextPassword: Boolean, @ColumnInfo(name = "forcePlainTextPassword") var forcePlainTextPassword: Boolean,
@ColumnInfo(name = "musicFolderId") var musicFolderId: String?, @ColumnInfo(name = "musicFolderId") var musicFolderId: String?,
@ColumnInfo(name = "minimumApiVersion") var minimumApiVersion: String?, @ColumnInfo(name = "minimumApiVersion") var minimumApiVersion: String?,
@ColumnInfo(name = "chatSupport") var chatSupport: Boolean? = null, @ColumnInfo(name = "chatSupport") var chatSupport: Boolean? = null,
@ColumnInfo(name = "bookmarkSupport") var bookmarkSupport: Boolean? = null, @ColumnInfo(name = "bookmarkSupport") var bookmarkSupport: Boolean? = null,
@ColumnInfo(name = "shareSupport") var shareSupport: Boolean? = null, @ColumnInfo(name = "shareSupport") var shareSupport: Boolean? = null,
@ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null @ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null,
@ColumnInfo(name = "jukeboxSupport") var jukeboxSupport: Boolean? = null,
@ColumnInfo(name = "videoSupport") var videoSupport: Boolean? = null
) { ) {
constructor() : this ( constructor() : this (
0, 0, "", "", null, "", "", false, false, false, null, null 0, 0, "", "", null, "", "", false, false, false, null, null
) )

View File

@ -13,8 +13,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
@ -23,32 +26,25 @@ import com.skydoves.colorpickerview.ColorPickerDialog
import com.skydoves.colorpickerview.flag.BubbleFlag import com.skydoves.colorpickerview.flag.BubbleFlag
import com.skydoves.colorpickerview.flag.FlagMode import com.skydoves.colorpickerview.flag.FlagMode
import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener
import java.io.IOException
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URL import java.net.URL
import java.util.Locale import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.api.subsonic.falseOnFailure
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.api.subsonic.throwOnFailure
import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.model.EditServerModel
import org.moire.ultrasonic.model.ServerSettingsModel import org.moire.ultrasonic.model.ServerSettingsModel
import org.moire.ultrasonic.service.MusicServiceFactory import org.moire.ultrasonic.service.MusicServiceFactory
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.CommunicationError.getErrorMessage
import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.ErrorDialog
import org.moire.ultrasonic.util.InfoDialog import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.ModalBackgroundTask
import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Util import org.moire.ultrasonic.util.Util
import retrofit2.Response
import timber.log.Timber import timber.log.Timber
private const val DIALOG_PADDING = 12 private const val DIALOG_PADDING = 12
@ -78,6 +74,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
private var selectedColor: Int? = null private var selectedColor: Int? = null
private val navArgs by navArgs<EditServerFragmentArgs>() private val navArgs by navArgs<EditServerFragmentArgs>()
val model: EditServerModel by viewModels()
@Override @Override
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -372,149 +369,82 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
/** /**
* Tests if the network connection to the entered Server Settings can be made * Tests if the network connection to the entered Server Settings can be made
*/ */
@Suppress("TooGenericExceptionCaught")
private fun testConnection() { private fun testConnection() {
val task: ModalBackgroundTask<String> = object : ModalBackgroundTask<String>( val testSetting = ServerSetting()
activity, val builder = InfoDialog.Builder(requireContext())
false builder.setTitle(R.string.supported_server_features)
) { builder.setMessage(getProgress(testSetting))
fun boolToMark(value: Boolean?): String { val dialog: AlertDialog = builder.create()
if (value == null) dialog.show()
return ""
return if (value) "✔️" else ""
}
fun getProgress(): String { val testJob = lifecycleScope.launch {
return String.format( try {
""" val flow = model.queryFeatureSupport(currentServerSetting!!).flowOn(Dispatchers.IO)
flow.collect {
model.storeFeatureSupport(testSetting, it)
dialog.setMessage(getProgress(testSetting))
Timber.w("${it.type} support: ${it.supported}")
}
currentServerSetting!!.chatSupport = testSetting.chatSupport
currentServerSetting!!.bookmarkSupport = testSetting.bookmarkSupport
currentServerSetting!!.shareSupport = testSetting.shareSupport
currentServerSetting!!.podcastSupport = testSetting.podcastSupport
currentServerSetting!!.videoSupport = testSetting.videoSupport
currentServerSetting!!.jukeboxSupport = testSetting.jukeboxSupport
} catch (cancellationException: CancellationException) {
Timber.i(cancellationException)
} catch (exception: Exception) {
dialog.dismiss()
Timber.w(exception)
ErrorDialog.Builder(requireContext())
.setTitle(R.string.error_label)
.setMessage(getErrorMessage(exception, context))
.show()
}
}
dialog.setOnDismissListener { testJob.cancel() }
}
private fun getProgress(serverSetting: ServerSetting): String {
val isAnyDisabled = arrayOf(
serverSetting.chatSupport,
serverSetting.bookmarkSupport,
serverSetting.shareSupport,
serverSetting.podcastSupport,
serverSetting.videoSupport,
serverSetting.jukeboxSupport,
).any { x -> x == false }
var progressString = String.format(
"""
|%s - ${resources.getString(R.string.button_bar_chat)} |%s - ${resources.getString(R.string.button_bar_chat)}
|%s - ${resources.getString(R.string.button_bar_bookmarks)} |%s - ${resources.getString(R.string.button_bar_bookmarks)}
|%s - ${resources.getString(R.string.button_bar_shares)} |%s - ${resources.getString(R.string.button_bar_shares)}
|%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.jukebox)}
""".trimMargin(), """.trimMargin(),
boolToMark(currentServerSetting!!.chatSupport), boolToMark(serverSetting.chatSupport),
boolToMark(currentServerSetting!!.bookmarkSupport), boolToMark(serverSetting.bookmarkSupport),
boolToMark(currentServerSetting!!.shareSupport), boolToMark(serverSetting.shareSupport),
boolToMark(currentServerSetting!!.podcastSupport) boolToMark(serverSetting.podcastSupport),
) boolToMark(serverSetting.videoSupport),
} boolToMark(serverSetting.jukeboxSupport)
)
if (isAnyDisabled)
progressString += "\n\n" + resources.getString(R.string.server_editor_disabled_feature)
@Throws(Throwable::class) return progressString
override fun doInBackground(): String {
currentServerSetting!!.chatSupport = null
currentServerSetting!!.bookmarkSupport = null
currentServerSetting!!.shareSupport = null
currentServerSetting!!.podcastSupport = null
updateProgress(getProgress())
val configuration = SubsonicClientConfiguration(
currentServerSetting!!.url,
currentServerSetting!!.userName,
currentServerSetting!!.password,
SubsonicAPIVersions.getClosestKnownClientApiVersion(
Constants.REST_PROTOCOL_VERSION
),
Constants.REST_CLIENT_ID,
currentServerSetting!!.allowSelfSignedCertificate,
currentServerSetting!!.forcePlainTextPassword,
BuildConfig.DEBUG
)
val subsonicApiClient = SubsonicAPIClient(configuration)
// Execute a ping to retrieve the API version.
// This is accepted to fail if the authentication is incorrect yet.
var pingResponse = subsonicApiClient.api.ping().execute()
if (pingResponse.body() != null) {
val restApiVersion = pingResponse.body()!!.version.restApiVersion
currentServerSetting!!.minimumApiVersion = restApiVersion
Timber.i("Server minimum API version set to %s", restApiVersion)
}
// Execute a ping to check the authentication, now using the correct API version.
pingResponse = subsonicApiClient.api.ping().execute()
pingResponse.throwOnFailure()
currentServerSetting!!.chatSupport = isServerFunctionAvailable {
subsonicApiClient.api.getChatMessages().execute()
}
updateProgress(getProgress())
currentServerSetting!!.bookmarkSupport = isServerFunctionAvailable {
subsonicApiClient.api.getBookmarks().execute()
}
updateProgress(getProgress())
currentServerSetting!!.shareSupport = isServerFunctionAvailable {
subsonicApiClient.api.getShares().execute()
}
updateProgress(getProgress())
currentServerSetting!!.podcastSupport = isServerFunctionAvailable {
subsonicApiClient.api.getPodcasts().execute()
}
updateProgress(getProgress())
val licenseResponse = subsonicApiClient.api.getLicense().execute()
licenseResponse.throwOnFailure()
if (!licenseResponse.body()!!.license.valid) {
return getProgress() + "\n" +
resources.getString(R.string.settings_testing_unlicensed)
}
return getProgress()
}
override fun done(responseString: String) {
var dialogText = responseString
if (arrayOf(
currentServerSetting!!.chatSupport,
currentServerSetting!!.bookmarkSupport,
currentServerSetting!!.shareSupport,
currentServerSetting!!.podcastSupport
).any { x -> x == false }
) {
dialogText = String.format(
Locale.ROOT,
"%s\n\n%s",
responseString,
resources.getString(R.string.server_editor_disabled_feature)
)
}
InfoDialog.Builder(requireActivity())
.setTitle(R.string.settings_testing_ok)
.setMessage(dialogText)
.show()
}
override fun error(error: Throwable) {
Timber.w(error)
ErrorDialog(
context = activity,
message = String.format(
"%s %s",
resources.getString(R.string.settings_connection_failure),
getErrorMessage(error)
)
).show()
}
}
task.execute()
} }
private fun isServerFunctionAvailable(function: () -> Response<out SubsonicResponse>): Boolean { private fun boolToMark(value: Boolean?): String {
return try { if (value == null)
function().falseOnFailure() return ""
} catch (_: IOException) { return if (value) "✔️" else ""
false
} catch (_: SubsonicRESTException) {
false
}
} }
/** /**
@ -522,7 +452,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
*/ */
private fun finishActivity() { private fun finishActivity() {
if (areFieldsChanged()) { if (areFieldsChanged()) {
ErrorDialog.Builder(context) ErrorDialog.Builder(requireContext())
.setTitle(R.string.common_confirm) .setTitle(R.string.common_confirm)
.setMessage(R.string.server_editor_leave_confirmation) .setMessage(R.string.server_editor_leave_confirmation)
.setPositiveButton(R.string.common_ok) { dialog, _ -> .setPositiveButton(R.string.common_ok) { dialog, _ ->

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.fragment package org.moire.ultrasonic.fragment
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
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
@ -90,6 +89,7 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError import org.moire.ultrasonic.util.CommunicationError
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.toTrack import org.moire.ultrasonic.util.toTrack
@ -1256,12 +1256,14 @@ class PlayerFragment :
mediaPlayerController.setSongRating(rating) mediaPlayerController.setSongRating(rating)
} }
@SuppressLint("InflateParams")
private fun showSavePlaylistDialog() { private fun showSavePlaylistDialog() {
val layout = LayoutInflater.from(this.context).inflate(R.layout.save_playlist, null) val layout = LayoutInflater.from(this.context)
.inflate(R.layout.save_playlist, null)
playlistNameView = layout.findViewById(R.id.save_playlist_name) playlistNameView = layout.findViewById(R.id.save_playlist_name)
val builder: AlertDialog.Builder = AlertDialog.Builder(context) val builder = ConfirmationDialog.Builder(requireContext())
builder.setTitle(R.string.download_playlist_title) builder.setTitle(R.string.download_playlist_title)
builder.setMessage(R.string.download_playlist_name) builder.setMessage(R.string.download_playlist_name)

View File

@ -88,7 +88,8 @@ class ServerSelectorFragment : Fragment() {
* This Callback handles the deletion of a Server Setting * This Callback handles the deletion of a Server Setting
*/ */
private fun deleteServerById(id: Int) { private fun deleteServerById(id: Int) {
ErrorDialog.Builder(context) // FIXME
ErrorDialog.Builder(requireContext())
.setTitle(R.string.server_menu_delete) .setTitle(R.string.server_menu_delete)
.setMessage(R.string.server_selector_delete_confirmation) .setMessage(R.string.server_selector_delete_confirmation)
.setPositiveButton(R.string.common_delete) { dialog, _ -> .setPositiveButton(R.string.common_delete) { dialog, _ ->

View File

@ -1,7 +1,6 @@
package org.moire.ultrasonic.fragment package org.moire.ultrasonic.fragment
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@ -34,6 +33,7 @@ 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.MediaPlayerController import org.moire.ultrasonic.service.MediaPlayerController
import org.moire.ultrasonic.service.RxBus import org.moire.ultrasonic.service.RxBus
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Constants
import org.moire.ultrasonic.util.ErrorDialog import org.moire.ultrasonic.util.ErrorDialog
import org.moire.ultrasonic.util.FileUtil.ultrasonicDirectory import org.moire.ultrasonic.util.FileUtil.ultrasonicDirectory
@ -136,7 +136,7 @@ class SettingsFragment :
return return
} }
} }
ErrorDialog.Builder(context) ErrorDialog.Builder(requireContext())
.setMessage(R.string.settings_cache_location_error) .setMessage(R.string.settings_cache_location_error)
.show() .show()
} }
@ -297,7 +297,7 @@ class SettingsFragment :
onChosen: (Int) -> Unit onChosen: (Int) -> Unit
) { ) {
val choice = intArrayOf(defaultChoice) val choice = intArrayOf(defaultChoice)
AlertDialog.Builder(activity).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 }
@ -404,7 +404,7 @@ class SettingsFragment :
) )
val keep = R.string.settings_debug_log_keep val keep = R.string.settings_debug_log_keep
val delete = R.string.settings_debug_log_delete val delete = R.string.settings_debug_log_delete
InfoDialog.Builder(activity) ConfirmationDialog.Builder(requireContext())
.setMessage(message) .setMessage(message)
.setNegativeButton(keep) { dIf: DialogInterface, _: Int -> .setNegativeButton(keep) { dIf: DialogInterface, _: Int ->
dIf.cancel() dIf.cancel()
@ -413,7 +413,7 @@ class SettingsFragment :
deleteLogFiles() deleteLogFiles()
Timber.i("Deleted debug log files") Timber.i("Deleted debug log files")
dIf.dismiss() dIf.dismiss()
AlertDialog.Builder(activity) InfoDialog.Builder(requireContext())
.setMessage(R.string.settings_debug_log_deleted) .setMessage(R.string.settings_debug_log_deleted)
.setPositiveButton(R.string.common_ok) { dIf2: DialogInterface, _: Int -> .setPositiveButton(R.string.common_ok) { dIf2: DialogInterface, _: Int ->
dIf2.dismiss() dIf2.dismiss()

View File

@ -229,7 +229,7 @@ open class TrackCollectionFragment(
unpinButton?.setOnClickListener { unpinButton?.setOnClickListener {
if (Settings.showConfirmationDialog) { if (Settings.showConfirmationDialog) {
ConfirmationDialog.Builder(context) ConfirmationDialog.Builder(requireContext())
.setMessage(R.string.common_unpin_selection_confirmation) .setMessage(R.string.common_unpin_selection_confirmation)
.setPositiveButton(R.string.common_unpin) { _, _ -> .setPositiveButton(R.string.common_unpin) { _, _ ->
unpin() unpin()
@ -245,7 +245,7 @@ open class TrackCollectionFragment(
deleteButton?.setOnClickListener { deleteButton?.setOnClickListener {
if (Settings.showConfirmationDialog) { if (Settings.showConfirmationDialog) {
ConfirmationDialog.Builder(context) ConfirmationDialog.Builder(requireContext())
.setMessage(R.string.common_delete_selection_confirmation) .setMessage(R.string.common_delete_selection_confirmation)
.setPositiveButton(R.string.common_delete) { _, _ -> .setPositiveButton(R.string.common_delete) { _, _ ->
delete() delete()

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.fragment.legacy package org.moire.ultrasonic.fragment.legacy
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle import android.os.Bundle
import android.text.Spannable import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
@ -43,7 +42,9 @@ import org.moire.ultrasonic.subsonic.DownloadHandler
import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.CacheCleaner
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.LoadingTask import org.moire.ultrasonic.util.LoadingTask
import org.moire.ultrasonic.util.Util.applyTheme import org.moire.ultrasonic.util.Util.applyTheme
import org.moire.ultrasonic.util.Util.toast import org.moire.ultrasonic.util.Util.toast
@ -222,7 +223,7 @@ class PlaylistsFragment : Fragment() {
} }
private fun deletePlaylist(playlist: Playlist) { private fun deletePlaylist(playlist: Playlist) {
AlertDialog.Builder(context).setIcon(R.drawable.ic_baseline_warning) ConfirmationDialog.Builder(requireContext()).setIcon(R.drawable.ic_baseline_warning)
.setTitle(R.string.common_confirm).setMessage( .setTitle(R.string.common_confirm).setMessage(
resources.getString(R.string.delete_playlist, playlist.name) resources.getString(R.string.delete_playlist, playlist.name)
).setPositiveButton(R.string.common_ok) { _, _ -> ).setPositiveButton(R.string.common_ok) { _, _ ->
@ -283,8 +284,8 @@ class PlaylistsFragment : Fragment() {
Linkify.addLinks(message, Linkify.WEB_URLS) Linkify.addLinks(message, Linkify.WEB_URLS)
textView.text = message textView.text = message
textView.movementMethod = LinkMovementMethod.getInstance() textView.movementMethod = LinkMovementMethod.getInstance()
AlertDialog.Builder(context).setTitle(playlist.name).setCancelable(true) InfoDialog.Builder(requireContext()).setTitle(playlist.name).setCancelable(true)
.setIcon(R.drawable.ic_baseline_info).setView(textView).show() .setView(textView).show()
} }
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@ -301,7 +302,7 @@ class PlaylistsFragment : Fragment() {
} else { } else {
publicBox.isChecked = pub publicBox.isChecked = pub
} }
val alertDialog = AlertDialog.Builder(context) val alertDialog = ConfirmationDialog.Builder(requireContext())
alertDialog.setIcon(R.drawable.ic_baseline_warning) alertDialog.setIcon(R.drawable.ic_baseline_warning)
alertDialog.setTitle(R.string.playlist_update_info) alertDialog.setTitle(R.string.playlist_update_info)
alertDialog.setView(dialogView) alertDialog.setView(dialogView)

View File

@ -0,0 +1,155 @@
/*
* EditServerModel.kt
* Copyright (C) 2009-2022 Ultrasonic developers
*
* Distributed under terms of the GNU GPLv3 license.
*/
package org.moire.ultrasonic.model
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import java.io.IOException
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.flatMapMerge
import kotlinx.coroutines.flow.flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import org.moire.ultrasonic.BuildConfig
import org.moire.ultrasonic.api.subsonic.SubsonicAPIClient
import org.moire.ultrasonic.api.subsonic.SubsonicAPIDefinition
import org.moire.ultrasonic.api.subsonic.SubsonicAPIVersions
import org.moire.ultrasonic.api.subsonic.SubsonicClientConfiguration
import org.moire.ultrasonic.api.subsonic.SubsonicRESTException
import org.moire.ultrasonic.api.subsonic.response.SubsonicResponse
import org.moire.ultrasonic.data.ActiveServerProvider
import org.moire.ultrasonic.data.ServerSetting
import org.moire.ultrasonic.util.Constants
import retrofit2.HttpException
import timber.log.Timber
@Suppress("MagicNumber")
class EditServerModel(val app: Application) : AndroidViewModel(app), KoinComponent {
val activeServerProvider: ActiveServerProvider by inject()
private suspend fun serverFunctionAvailable(
type: ServerFeature,
function: suspend () -> SubsonicResponse
): FeatureSupport {
val result = try {
function().falseOnFailure()
} catch (_: IOException) {
false
} catch (_: SubsonicRESTException) {
false
} catch (_: HttpException) {
false
}
return FeatureSupport(type, result)
}
/**
* This extension checks API call results for errors, API version, etc
* @return Boolean: True if everything was ok, false if an error was found
*/
private fun SubsonicResponse.falseOnFailure(): Boolean {
return (this.status === SubsonicResponse.Status.OK)
}
private fun requestFlow(
type: ServerFeature,
api: SubsonicAPIDefinition,
userName: String
) = flow {
when (type) {
ServerFeature.CHAT -> emit(
serverFunctionAvailable(type, api::getChatMessagesSuspend)
)
ServerFeature.BOOKMARK -> emit(
serverFunctionAvailable(type, api::getBookmarksSuspend)
)
ServerFeature.SHARE -> emit(
serverFunctionAvailable(type, api::getSharesSuspend)
)
ServerFeature.PODCAST -> emit(
serverFunctionAvailable(type, api::getPodcastsSuspend)
)
ServerFeature.JUKEBOX -> emit(
serverFunctionAvailable(type) {
val response = api.getUserSuspend(userName)
if (!response.user.jukeboxRole) throw IOException()
response
}
)
ServerFeature.VIDEO -> emit(
serverFunctionAvailable(type, api::getVideosSuspend)
)
}
}
@OptIn(FlowPreview::class)
suspend fun queryFeatureSupport(currentServerSetting: ServerSetting): Flow<FeatureSupport> {
val client = buildTestClient(currentServerSetting)
// One line of magic:
// Get all possible feature values, turn them into a flow,
// and execute each request concurrently
return (ServerFeature.values()).asFlow().flatMapMerge {
requestFlow(it, client.api, currentServerSetting.userName)
}
}
private suspend fun buildTestClient(serverSetting: ServerSetting): SubsonicAPIClient {
val configuration = SubsonicClientConfiguration(
serverSetting.url,
serverSetting.userName,
serverSetting.password,
SubsonicAPIVersions.getClosestKnownClientApiVersion(
Constants.REST_PROTOCOL_VERSION
),
Constants.REST_CLIENT_ID,
serverSetting.allowSelfSignedCertificate,
serverSetting.forcePlainTextPassword,
BuildConfig.DEBUG
)
val client = SubsonicAPIClient(configuration)
// Execute a ping to retrieve the API version.
// This is accepted to fail if the authentication is incorrect yet.
var pingResponse = client.api.pingSuspend()
val restApiVersion = pingResponse.version.restApiVersion
serverSetting.minimumApiVersion = restApiVersion
Timber.i("Server minimum API version set to %s", restApiVersion)
// Execute a ping to check the authentication, now using the correct API version.
pingResponse = client.api.pingSuspend()
return client
}
fun storeFeatureSupport(settings: ServerSetting, it: FeatureSupport) {
when (it.type) {
ServerFeature.CHAT -> settings.chatSupport = it.supported
ServerFeature.BOOKMARK -> settings.bookmarkSupport = it.supported
ServerFeature.SHARE -> settings.shareSupport = it.supported
ServerFeature.PODCAST -> settings.podcastSupport = it.supported
ServerFeature.JUKEBOX -> settings.jukeboxSupport = it.supported
ServerFeature.VIDEO -> settings.videoSupport = it.supported
}
}
companion object {
enum class ServerFeature(val named: String) {
CHAT("chat"),
BOOKMARK("bookmark"),
SHARE("share"),
PODCAST("podcast"),
JUKEBOX("jukebox"),
VIDEO("video")
}
data class FeatureSupport(val type: ServerFeature, val supported: Boolean)
}
}

View File

@ -7,7 +7,7 @@
package org.moire.ultrasonic.subsonic package org.moire.ultrasonic.subsonic
import android.app.AlertDialog import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
@ -27,6 +27,7 @@ import org.moire.ultrasonic.domain.Track
import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService import org.moire.ultrasonic.service.MusicServiceFactory.getMusicService
import org.moire.ultrasonic.util.BackgroundTask import org.moire.ultrasonic.util.BackgroundTask
import org.moire.ultrasonic.util.CancellationToken import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.FragmentBackgroundTask import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.ShareDetails import org.moire.ultrasonic.util.ShareDetails
@ -150,6 +151,8 @@ class ShareHandler(val context: Context) {
} }
} }
@Suppress("LongMethod")
@SuppressLint("InflateParams")
private fun showDialog( private fun showDialog(
fragment: Fragment, fragment: Fragment,
shareDetails: ShareDetails, shareDetails: ShareDetails,
@ -184,7 +187,7 @@ class ShareHandler(val context: Context) {
} }
updateVisibility() updateVisibility()
val builder = AlertDialog.Builder(fragment.context) val builder = ConfirmationDialog.Builder(fragment.requireContext())
builder.setTitle(R.string.share_set_share_options) builder.setTitle(R.string.share_set_share_options)
builder.setPositiveButton(R.string.menu_share) { _, _ -> builder.setPositiveButton(R.string.menu_share) { _, _ ->

View File

@ -8,10 +8,14 @@
package org.moire.ultrasonic.util package org.moire.ultrasonic.util
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.moire.ultrasonic.R import org.moire.ultrasonic.R
/*
* InfoDialog can be used to show some information to the user. Typically it cannot be cancelled,
* only dismissed via OK.
*/
open class InfoDialog( open class InfoDialog(
context: Context, context: Context,
message: CharSequence?, message: CharSequence?,
@ -19,7 +23,7 @@ open class InfoDialog(
private val finishActivityOnClose: Boolean = false private val finishActivityOnClose: Boolean = false
) { ) {
open var builder: AlertDialog.Builder = Builder(activity ?: context, message) open var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message)
fun show() { fun show() {
builder.setOnCancelListener { builder.setOnCancelListener {
@ -35,7 +39,7 @@ open class InfoDialog(
builder.create().show() builder.create().show()
} }
class Builder(context: Context?) : AlertDialog.Builder(context) { class Builder(context: Context) : MaterialAlertDialogBuilder(context) {
constructor(context: Context, message: CharSequence?) : this(context) { constructor(context: Context, message: CharSequence?) : this(context) {
setMessage(message) setMessage(message)
@ -44,7 +48,6 @@ open class InfoDialog(
init { init {
setIcon(R.drawable.ic_baseline_info) setIcon(R.drawable.ic_baseline_info)
setTitle(R.string.common_confirm) setTitle(R.string.common_confirm)
setCancelable(true)
setPositiveButton(R.string.common_ok) { _, _ -> setPositiveButton(R.string.common_ok) { _, _ ->
// Just close it // Just close it
} }
@ -52,6 +55,10 @@ open class InfoDialog(
} }
} }
/*
* ErrorDialog can be used to show some an error to the user.
* Typically it cannot be cancelled, only dismissed via OK.
*/
class ErrorDialog( class ErrorDialog(
context: Context, context: Context,
message: CharSequence?, message: CharSequence?,
@ -59,9 +66,9 @@ class ErrorDialog(
finishActivityOnClose: Boolean = false finishActivityOnClose: Boolean = false
) : InfoDialog(context, message, activity, finishActivityOnClose) { ) : InfoDialog(context, message, activity, finishActivityOnClose) {
override var builder: AlertDialog.Builder = Builder(activity ?: context, message) override var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message)
class Builder(context: Context?) : AlertDialog.Builder(context) { class Builder(context: Context) : MaterialAlertDialogBuilder(context) {
constructor(context: Context, message: CharSequence?) : this(context) { constructor(context: Context, message: CharSequence?) : this(context) {
setMessage(message) setMessage(message)
} }
@ -69,7 +76,6 @@ class ErrorDialog(
init { init {
setIcon(R.drawable.ic_baseline_warning) setIcon(R.drawable.ic_baseline_warning)
setTitle(R.string.error_label) setTitle(R.string.error_label)
setCancelable(true)
setPositiveButton(R.string.common_ok) { _, _ -> setPositiveButton(R.string.common_ok) { _, _ ->
// Just close it // Just close it
} }
@ -77,6 +83,10 @@ class ErrorDialog(
} }
} }
/*
* ConfirmationDialog can be used to present a choice to the user.
* Typically it will be cancelable..
*/
class ConfirmationDialog( class ConfirmationDialog(
context: Context, context: Context,
message: CharSequence?, message: CharSequence?,
@ -84,9 +94,9 @@ class ConfirmationDialog(
finishActivityOnClose: Boolean = false finishActivityOnClose: Boolean = false
) : InfoDialog(context, message, activity, finishActivityOnClose) { ) : InfoDialog(context, message, activity, finishActivityOnClose) {
override var builder: AlertDialog.Builder = Builder(activity ?: context, message) override var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message)
class Builder(context: Context?) : AlertDialog.Builder(context) { class Builder(context: Context) : MaterialAlertDialogBuilder(context) {
constructor(context: Context, message: CharSequence?) : this(context) { constructor(context: Context, message: CharSequence?) : this(context) {
setMessage(message) setMessage(message)
} }

View File

@ -157,7 +157,6 @@
<string name="settings.clear_bookmark">Zahodit záložku</string> <string name="settings.clear_bookmark">Zahodit záložku</string>
<string name="settings.clear_bookmark_summary">Zahodit záložku po dokončení přehrávání skladby</string> <string name="settings.clear_bookmark_summary">Zahodit záložku po dokončení přehrávání skladby</string>
<string name="settings.clear_search_history">Vyčistit historii vyhledávání</string> <string name="settings.clear_search_history">Vyčistit historii vyhledávání</string>
<string name="settings.connection_failure">Chyba připojení.</string>
<string name="settings.default_albums">Výchozí alba</string> <string name="settings.default_albums">Výchozí alba</string>
<string name="settings.default_artists">Výchozí umělci</string> <string name="settings.default_artists">Výchozí umělci</string>
<string name="settings.default_songs">Výchozí skladby</string> <string name="settings.default_songs">Výchozí skladby</string>
@ -248,8 +247,6 @@
<string name="settings.show_track_number">Zobrazovat číslo skladby</string> <string name="settings.show_track_number">Zobrazovat číslo skladby</string>
<string name="settings.show_track_number_summary">Připojovat číslo skladby při zobrazování skladby</string> <string name="settings.show_track_number_summary">Připojovat číslo skladby při zobrazování skladby</string>
<string name="settings.test_connection_title">Test připojení</string> <string name="settings.test_connection_title">Test připojení</string>
<string name="settings.testing_ok">Připojení je v pořádku</string>
<string name="settings.testing_unlicensed">Připojení je v pořádku. Server bez licence.</string>
<string name="settings.theme_light">Světlý</string> <string name="settings.theme_light">Světlý</string>
<string name="settings.theme_dark">Tmavý</string> <string name="settings.theme_dark">Tmavý</string>
<string name="settings.theme_black">Černý</string> <string name="settings.theme_black">Černý</string>

View File

@ -196,7 +196,6 @@
<string name="settings.clear_bookmark">Lesezeichen löschen</string> <string name="settings.clear_bookmark">Lesezeichen löschen</string>
<string name="settings.clear_bookmark_summary">Lesezeichen nach Wiedergabe löschen</string> <string name="settings.clear_bookmark_summary">Lesezeichen nach Wiedergabe löschen</string>
<string name="settings.clear_search_history">Suchverlauf löschen</string> <string name="settings.clear_search_history">Suchverlauf löschen</string>
<string name="settings.connection_failure">Verbindungsfehler</string>
<string name="settings.default_albums">Anzahl der Alben</string> <string name="settings.default_albums">Anzahl der Alben</string>
<string name="settings.default_artists">Anzahl der Künstler*innen</string> <string name="settings.default_artists">Anzahl der Künstler*innen</string>
<string name="settings.default_songs">Anzahl der Titel</string> <string name="settings.default_songs">Anzahl der Titel</string>
@ -297,8 +296,6 @@
<string name="settings.show_track_number">Titelnummer anzeigen</string> <string name="settings.show_track_number">Titelnummer anzeigen</string>
<string name="settings.show_track_number_summary">Titel mit Nummer anzeigen</string> <string name="settings.show_track_number_summary">Titel mit Nummer anzeigen</string>
<string name="settings.test_connection_title">Verbindung testen</string> <string name="settings.test_connection_title">Verbindung testen</string>
<string name="settings.testing_ok">Verbindung OK</string>
<string name="settings.testing_unlicensed">Verbindung OK, Server nicht lizenziert.</string>
<string name="settings.theme_light">Hell</string> <string name="settings.theme_light">Hell</string>
<string name="settings.theme_dark">Dunkel</string> <string name="settings.theme_dark">Dunkel</string>
<string name="settings.theme_black">Schwarz</string> <string name="settings.theme_black">Schwarz</string>

View File

@ -198,7 +198,6 @@
<string name="settings.clear_bookmark">Limpiar marcador</string> <string name="settings.clear_bookmark">Limpiar marcador</string>
<string name="settings.clear_bookmark_summary">Limpiar marcador tras la finalización de la reproducción de una canción</string> <string name="settings.clear_bookmark_summary">Limpiar marcador tras la finalización de la reproducción de una canción</string>
<string name="settings.clear_search_history">Limpiar el historial de búsqueda</string> <string name="settings.clear_search_history">Limpiar el historial de búsqueda</string>
<string name="settings.connection_failure">Fallo de conexión.</string>
<string name="settings.default_albums">Álbumes predeterminados</string> <string name="settings.default_albums">Álbumes predeterminados</string>
<string name="settings.default_artists">Artistas predeterminados</string> <string name="settings.default_artists">Artistas predeterminados</string>
<string name="settings.default_songs">Canciones predeterminadas</string> <string name="settings.default_songs">Canciones predeterminadas</string>
@ -299,8 +298,6 @@
<string name="settings.show_track_number">Mostrar número de pista</string> <string name="settings.show_track_number">Mostrar número de pista</string>
<string name="settings.show_track_number_summary">Incluir el número de pista cuando se muestre una canción</string> <string name="settings.show_track_number_summary">Incluir el número de pista cuando se muestre una canción</string>
<string name="settings.test_connection_title">Comprobar conexión</string> <string name="settings.test_connection_title">Comprobar conexión</string>
<string name="settings.testing_ok">La conexión es correcta</string>
<string name="settings.testing_unlicensed">La conexión es correcta. Servidor sin licencia.</string>
<string name="settings.theme_day_night">Día y noche</string> <string name="settings.theme_day_night">Día y noche</string>
<string name="settings.theme_light">Claro</string> <string name="settings.theme_light">Claro</string>
<string name="settings.theme_dark">Oscuro</string> <string name="settings.theme_dark">Oscuro</string>

View File

@ -194,7 +194,6 @@
<string name="settings.clear_bookmark">Effacer le signet</string> <string name="settings.clear_bookmark">Effacer le signet</string>
<string name="settings.clear_bookmark_summary">Effacer le signet à la fin de la lecture d\'un titre</string> <string name="settings.clear_bookmark_summary">Effacer le signet à la fin de la lecture d\'un titre</string>
<string name="settings.clear_search_history">Effacer l\'historique des recherches</string> <string name="settings.clear_search_history">Effacer l\'historique des recherches</string>
<string name="settings.connection_failure">Échec de la connexion</string>
<string name="settings.default_albums">Albums par défaut</string> <string name="settings.default_albums">Albums par défaut</string>
<string name="settings.default_artists">Artistes par défaut</string> <string name="settings.default_artists">Artistes par défaut</string>
<string name="settings.default_songs">Musiques par défaut</string> <string name="settings.default_songs">Musiques par défaut</string>
@ -291,8 +290,6 @@
<string name="settings.show_track_number">Afficher le numéro du titre</string> <string name="settings.show_track_number">Afficher le numéro du titre</string>
<string name="settings.show_track_number_summary">Inclure son numéro lors de l\'affichage d\'un titre</string> <string name="settings.show_track_number_summary">Inclure son numéro lors de l\'affichage d\'un titre</string>
<string name="settings.test_connection_title">Tester la connexion</string> <string name="settings.test_connection_title">Tester la connexion</string>
<string name="settings.testing_ok">Connexion correcte</string>
<string name="settings.testing_unlicensed">Connexion correcte. Serveur sans licence.</string>
<string name="settings.theme_light">Clair</string> <string name="settings.theme_light">Clair</string>
<string name="settings.theme_dark">Sombre</string> <string name="settings.theme_dark">Sombre</string>
<string name="settings.theme_black">Noir</string> <string name="settings.theme_black">Noir</string>

View File

@ -163,7 +163,6 @@
<string name="settings.clear_bookmark">Könyvjelző törlése</string> <string name="settings.clear_bookmark">Könyvjelző törlése</string>
<string name="settings.clear_bookmark_summary">Könyvjelző törlése a dal lejátszása után.</string> <string name="settings.clear_bookmark_summary">Könyvjelző törlése a dal lejátszása után.</string>
<string name="settings.clear_search_history">Keresési előzmények törlése</string> <string name="settings.clear_search_history">Keresési előzmények törlése</string>
<string name="settings.connection_failure">Csatlakozási hiba!</string>
<string name="settings.default_albums">Albumok találati száma</string> <string name="settings.default_albums">Albumok találati száma</string>
<string name="settings.default_artists">Előadók találati száma</string> <string name="settings.default_artists">Előadók találati száma</string>
<string name="settings.default_songs">Dalok találati száma</string> <string name="settings.default_songs">Dalok találati száma</string>
@ -256,8 +255,6 @@
<string name="settings.show_track_number">Sorszám megjelenítése</string> <string name="settings.show_track_number">Sorszám megjelenítése</string>
<string name="settings.show_track_number_summary">Dalok sorszámának megjelenítése.</string> <string name="settings.show_track_number_summary">Dalok sorszámának megjelenítése.</string>
<string name="settings.test_connection_title">Kapcsolat tesztelése</string> <string name="settings.test_connection_title">Kapcsolat tesztelése</string>
<string name="settings.testing_ok">Kapcsolat OK!</string>
<string name="settings.testing_unlicensed">Kapcsolat OK! A kiszolgálónak nincs licence!</string>
<string name="settings.theme_light">Világos</string> <string name="settings.theme_light">Világos</string>
<string name="settings.theme_dark">Sötét</string> <string name="settings.theme_dark">Sötét</string>
<string name="settings.theme_black">Fekete</string> <string name="settings.theme_black">Fekete</string>

View File

@ -154,7 +154,6 @@
<string name="settings.clear_bookmark">Pulisci Segnalibro</string> <string name="settings.clear_bookmark">Pulisci Segnalibro</string>
<string name="settings.clear_bookmark_summary">Pulisci segnalibro al completamento della riproduzione di una canzone</string> <string name="settings.clear_bookmark_summary">Pulisci segnalibro al completamento della riproduzione di una canzone</string>
<string name="settings.clear_search_history">Pulisci Storico Ricerca</string> <string name="settings.clear_search_history">Pulisci Storico Ricerca</string>
<string name="settings.connection_failure">Errore connessione.</string>
<string name="settings.default_albums">Album predefiniti</string> <string name="settings.default_albums">Album predefiniti</string>
<string name="settings.default_artists">Artisti predefiniti</string> <string name="settings.default_artists">Artisti predefiniti</string>
<string name="settings.default_songs">Canzoni predefinte</string> <string name="settings.default_songs">Canzoni predefinte</string>
@ -240,8 +239,6 @@
<string name="settings.show_track_number">Visualizza numero traccia</string> <string name="settings.show_track_number">Visualizza numero traccia</string>
<string name="settings.show_track_number_summary">Includi numero traccia quando visualizzi una canzone</string> <string name="settings.show_track_number_summary">Includi numero traccia quando visualizzi una canzone</string>
<string name="settings.test_connection_title">Prova Connessione</string> <string name="settings.test_connection_title">Prova Connessione</string>
<string name="settings.testing_ok">Connessione OK</string>
<string name="settings.testing_unlicensed">Connessione OK. Server senza licenza.</string>
<string name="settings.theme_light">Chiaro</string> <string name="settings.theme_light">Chiaro</string>
<string name="settings.theme_dark">Scuro</string> <string name="settings.theme_dark">Scuro</string>
<string name="settings.theme_title">Tema</string> <string name="settings.theme_title">Tema</string>

View File

@ -198,7 +198,6 @@
<string name="settings.clear_bookmark">Bladwijzer verwijderen</string> <string name="settings.clear_bookmark">Bladwijzer verwijderen</string>
<string name="settings.clear_bookmark_summary">Bladwijzer verwijderen nadat nummer is afgespeeld</string> <string name="settings.clear_bookmark_summary">Bladwijzer verwijderen nadat nummer is afgespeeld</string>
<string name="settings.clear_search_history">Zoekgeschiedenis wissen</string> <string name="settings.clear_search_history">Zoekgeschiedenis wissen</string>
<string name="settings.connection_failure">Verbindingsfout.</string>
<string name="settings.default_albums">Standaardalbums</string> <string name="settings.default_albums">Standaardalbums</string>
<string name="settings.default_artists">Standaardartiesten</string> <string name="settings.default_artists">Standaardartiesten</string>
<string name="settings.default_songs">Standaardnummers</string> <string name="settings.default_songs">Standaardnummers</string>
@ -299,8 +298,6 @@
<string name="settings.show_track_number">Itemnummer tonen</string> <string name="settings.show_track_number">Itemnummer tonen</string>
<string name="settings.show_track_number_summary">Itemnummer tonen tijdens tonen van nummers</string> <string name="settings.show_track_number_summary">Itemnummer tonen tijdens tonen van nummers</string>
<string name="settings.test_connection_title">Verbinding testen</string> <string name="settings.test_connection_title">Verbinding testen</string>
<string name="settings.testing_ok">Verbinding is goed</string>
<string name="settings.testing_unlicensed">Verbinding is goed; geen serverlicentie.</string>
<string name="settings.theme_day_night">Dag en nacht</string> <string name="settings.theme_day_night">Dag en nacht</string>
<string name="settings.theme_light">Licht</string> <string name="settings.theme_light">Licht</string>
<string name="settings.theme_dark">Donker</string> <string name="settings.theme_dark">Donker</string>

View File

@ -157,7 +157,6 @@
<string name="settings.clear_bookmark">Czyszczenie zakładek</string> <string name="settings.clear_bookmark">Czyszczenie zakładek</string>
<string name="settings.clear_bookmark_summary">Czyść zakładkę po zakończeniu odtwarzania utworu</string> <string name="settings.clear_bookmark_summary">Czyść zakładkę po zakończeniu odtwarzania utworu</string>
<string name="settings.clear_search_history">Wyczyść historię wyszukiwania</string> <string name="settings.clear_search_history">Wyczyść historię wyszukiwania</string>
<string name="settings.connection_failure">Błąd połączenia.</string>
<string name="settings.default_albums">Domyślna ilość wyników - albumy</string> <string name="settings.default_albums">Domyślna ilość wyników - albumy</string>
<string name="settings.default_artists">Domyślna ilość wyników - artyści</string> <string name="settings.default_artists">Domyślna ilość wyników - artyści</string>
<string name="settings.default_songs">Domyślna ilość wyników - utwory</string> <string name="settings.default_songs">Domyślna ilość wyników - utwory</string>
@ -248,8 +247,6 @@
<string name="settings.show_track_number">Wyświetlaj numer utworu</string> <string name="settings.show_track_number">Wyświetlaj numer utworu</string>
<string name="settings.show_track_number_summary">Dołącza numer utworu podczas wyświetlania utworu</string> <string name="settings.show_track_number_summary">Dołącza numer utworu podczas wyświetlania utworu</string>
<string name="settings.test_connection_title">Testuj połączenie</string> <string name="settings.test_connection_title">Testuj połączenie</string>
<string name="settings.testing_ok">Połączenie jest OK</string>
<string name="settings.testing_unlicensed">Połączenie jest OK. Brak licencji na serwerze.</string>
<string name="settings.theme_light">Jasny</string> <string name="settings.theme_light">Jasny</string>
<string name="settings.theme_dark">Ciemny</string> <string name="settings.theme_dark">Ciemny</string>
<string name="settings.theme_title">Motyw</string> <string name="settings.theme_title">Motyw</string>

View File

@ -196,7 +196,6 @@
<string name="settings.clear_bookmark">Limpar Favoritos</string> <string name="settings.clear_bookmark">Limpar Favoritos</string>
<string name="settings.clear_bookmark_summary">Limpar favoritos após terminar de tocar a música</string> <string name="settings.clear_bookmark_summary">Limpar favoritos após terminar de tocar a música</string>
<string name="settings.clear_search_history">Limpar Histórico de Pesquisas</string> <string name="settings.clear_search_history">Limpar Histórico de Pesquisas</string>
<string name="settings.connection_failure">Falha na conexão.</string>
<string name="settings.default_albums">Álbuns Padrões</string> <string name="settings.default_albums">Álbuns Padrões</string>
<string name="settings.default_artists">Artistas Padrões</string> <string name="settings.default_artists">Artistas Padrões</string>
<string name="settings.default_songs">Músicas Padrões</string> <string name="settings.default_songs">Músicas Padrões</string>
@ -297,8 +296,6 @@
<string name="settings.show_track_number">Mostrar o Número da Faixa</string> <string name="settings.show_track_number">Mostrar o Número da Faixa</string>
<string name="settings.show_track_number_summary">Incluir o número da faixa ao mostrar uma música</string> <string name="settings.show_track_number_summary">Incluir o número da faixa ao mostrar uma música</string>
<string name="settings.test_connection_title">Teste de Conexão</string> <string name="settings.test_connection_title">Teste de Conexão</string>
<string name="settings.testing_ok">Conexão OK</string>
<string name="settings.testing_unlicensed">Conexão OK. Servidor não licenciado.</string>
<string name="settings.theme_day_night">Dia &amp; Noite</string> <string name="settings.theme_day_night">Dia &amp; Noite</string>
<string name="settings.theme_light">Claro</string> <string name="settings.theme_light">Claro</string>
<string name="settings.theme_dark">Escuro</string> <string name="settings.theme_dark">Escuro</string>

View File

@ -157,7 +157,6 @@
<string name="settings.clear_bookmark">Limpar Favoritos</string> <string name="settings.clear_bookmark">Limpar Favoritos</string>
<string name="settings.clear_bookmark_summary">Limpar favoritos após terminar de tocar a música</string> <string name="settings.clear_bookmark_summary">Limpar favoritos após terminar de tocar a música</string>
<string name="settings.clear_search_history">Limpar Histórico de Pesquisas</string> <string name="settings.clear_search_history">Limpar Histórico de Pesquisas</string>
<string name="settings.connection_failure">Falha na conexão.</string>
<string name="settings.default_albums">Álbuns Padrões</string> <string name="settings.default_albums">Álbuns Padrões</string>
<string name="settings.default_artists">Artistas Padrões</string> <string name="settings.default_artists">Artistas Padrões</string>
<string name="settings.default_songs">Músicas Padrões</string> <string name="settings.default_songs">Músicas Padrões</string>
@ -248,8 +247,6 @@
<string name="settings.show_track_number">Mostrar o Número da Faixa</string> <string name="settings.show_track_number">Mostrar o Número da Faixa</string>
<string name="settings.show_track_number_summary">Incluir o número da faixa quando mostrando uma música</string> <string name="settings.show_track_number_summary">Incluir o número da faixa quando mostrando uma música</string>
<string name="settings.test_connection_title">Teste de Conexão</string> <string name="settings.test_connection_title">Teste de Conexão</string>
<string name="settings.testing_ok">Conexão OK</string>
<string name="settings.testing_unlicensed">Conexão OK. Servidor não licenciado.</string>
<string name="settings.theme_light">Claro</string> <string name="settings.theme_light">Claro</string>
<string name="settings.theme_dark">Escuro</string> <string name="settings.theme_dark">Escuro</string>
<string name="settings.theme_title">Tema</string> <string name="settings.theme_title">Tema</string>

View File

@ -181,7 +181,6 @@
<string name="settings.clear_bookmark">Очистить закладку</string> <string name="settings.clear_bookmark">Очистить закладку</string>
<string name="settings.clear_bookmark_summary">Очистить закладку после завершения воспроизведения песни</string> <string name="settings.clear_bookmark_summary">Очистить закладку после завершения воспроизведения песни</string>
<string name="settings.clear_search_history">Очистить историю поиска</string> <string name="settings.clear_search_history">Очистить историю поиска</string>
<string name="settings.connection_failure">Ошибка подключения.</string>
<string name="settings.default_albums">Альбомы по умолчанию</string> <string name="settings.default_albums">Альбомы по умолчанию</string>
<string name="settings.default_artists">Исполнители по умолчанию</string> <string name="settings.default_artists">Исполнители по умолчанию</string>
<string name="settings.default_songs">Треки по умолчанию</string> <string name="settings.default_songs">Треки по умолчанию</string>
@ -274,8 +273,6 @@
<string name="settings.show_track_number">Показать номер трека</string> <string name="settings.show_track_number">Показать номер трека</string>
<string name="settings.show_track_number_summary">Включить номер дорожки при отображении песни</string> <string name="settings.show_track_number_summary">Включить номер дорожки при отображении песни</string>
<string name="settings.test_connection_title">Тестовое соединение</string> <string name="settings.test_connection_title">Тестовое соединение</string>
<string name="settings.testing_ok">Успешное соединение</string>
<string name="settings.testing_unlicensed">Успешное соединение. Сервер нелицензионный.</string>
<string name="settings.theme_light">Светлая</string> <string name="settings.theme_light">Светлая</string>
<string name="settings.theme_dark">Темная</string> <string name="settings.theme_dark">Темная</string>
<string name="settings.theme_black">Черная</string> <string name="settings.theme_black">Черная</string>

View File

@ -181,7 +181,6 @@
<string name="settings.clear_bookmark">清空书签</string> <string name="settings.clear_bookmark">清空书签</string>
<string name="settings.clear_bookmark_summary">歌曲播放完毕后清除书签</string> <string name="settings.clear_bookmark_summary">歌曲播放完毕后清除书签</string>
<string name="settings.clear_search_history">清空搜索历史</string> <string name="settings.clear_search_history">清空搜索历史</string>
<string name="settings.connection_failure">连接失败</string>
<string name="settings.default_albums">默认专辑</string> <string name="settings.default_albums">默认专辑</string>
<string name="settings.default_artists">默认艺术家</string> <string name="settings.default_artists">默认艺术家</string>
<string name="settings.default_songs">默认音乐</string> <string name="settings.default_songs">默认音乐</string>
@ -277,8 +276,6 @@
<string name="settings.show_track_number">显示曲目编号</string> <string name="settings.show_track_number">显示曲目编号</string>
<string name="settings.show_track_number_summary">显示歌曲时包括曲目编号</string> <string name="settings.show_track_number_summary">显示歌曲时包括曲目编号</string>
<string name="settings.test_connection_title">测试连接</string> <string name="settings.test_connection_title">测试连接</string>
<string name="settings.testing_ok">连接正常</string>
<string name="settings.testing_unlicensed">连接正常, 服务器未授权。</string>
<string name="settings.theme_light">Light</string> <string name="settings.theme_light">Light</string>
<string name="settings.theme_dark">Dark</string> <string name="settings.theme_dark">Dark</string>
<string name="settings.theme_black">Black</string> <string name="settings.theme_black">Black</string>

View File

@ -198,7 +198,6 @@
<string name="settings.clear_bookmark">Clear Bookmark</string> <string name="settings.clear_bookmark">Clear Bookmark</string>
<string name="settings.clear_bookmark_summary">Clear bookmark upon completion of playback of a song</string> <string name="settings.clear_bookmark_summary">Clear bookmark upon completion of playback of a song</string>
<string name="settings.clear_search_history">Clear Search History</string> <string name="settings.clear_search_history">Clear Search History</string>
<string name="settings.connection_failure">Connection failure.</string>
<string name="settings.default_albums">Default Albums</string> <string name="settings.default_albums">Default Albums</string>
<string name="settings.default_artists">Default Artists</string> <string name="settings.default_artists">Default Artists</string>
<string name="settings.default_songs">Default Songs</string> <string name="settings.default_songs">Default Songs</string>
@ -299,8 +298,6 @@
<string name="settings.show_track_number">Show Track Number</string> <string name="settings.show_track_number">Show Track Number</string>
<string name="settings.show_track_number_summary">Include track number when displaying a song</string> <string name="settings.show_track_number_summary">Include track number when displaying a song</string>
<string name="settings.test_connection_title">Test Connection</string> <string name="settings.test_connection_title">Test Connection</string>
<string name="settings.testing_ok">Connection is OK</string>
<string name="settings.testing_unlicensed">Connection is OK. Server unlicensed.</string>
<string name="settings.theme_day_night">Day &amp; Night</string> <string name="settings.theme_day_night">Day &amp; Night</string>
<string name="settings.theme_light">Light</string> <string name="settings.theme_light">Light</string>
<string name="settings.theme_dark">Dark</string> <string name="settings.theme_dark">Dark</string>
@ -446,11 +443,12 @@
<!-- Subsonic features --> <!-- Subsonic features -->
<string name="settings.five_star_rating_title">Use five star rating for songs</string> <string name="settings.five_star_rating_title">Use five star rating for songs</string>
<string name="settings.five_star_rating_description">Use five star rating system for songs instead of simply starring/unstarring items.</string> <string name="settings.five_star_rating_description">Use five star rating system for songs instead of simply starring/unstarring items.</string>
<string name="settings.use_hw_offload_title">Use hardware playback (experimental)</string> <string name="settings.use_hw_offload_title">Use hardware playback (experimental)</string>
<string name="settings.use_hw_offload_description">Try to play the media using the media decoder chip on your phone. This can improve battery usage.</string> <string name="settings.use_hw_offload_description">Try to play the media using the media decoder chip on your phone. This can improve battery usage.</string>
<string name="list_view">List</string> <string name="list_view">List</string>
<string name="grid_view">Cover</string> <string name="grid_view">Cover</string>
<string name="supported_server_features">Supported features</string>
<string name="jukebox">Jukebox</string>
</resources> </resources>