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

This commit is contained in:
birdbird 2023-01-06 19:20:25 +00:00 committed by Nite
parent 2fafebee42
commit 7dea620e66
32 changed files with 455 additions and 232 deletions

View File

@ -1,7 +1,7 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<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" />
</inspection_tool>
</profile>

View File

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

View File

@ -66,6 +66,8 @@ style:
active: false
ReturnCount:
max: 5
ForbiddenImport:
imports: ['android.app.AlertDialog']
comments:
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.ListAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

View File

@ -19,10 +19,8 @@
package org.moire.ultrasonic.util;
import android.app.Activity;
import android.app.AlertDialog;
import androidx.appcompat.app.AlertDialog;
import org.moire.ultrasonic.R;
import timber.log.Timber;
/**
@ -47,9 +45,9 @@ public abstract class ModalBackgroundTask<T> extends BackgroundTask<T>
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.setMessage(R.string.background_task_loading);
builder.setOnCancelListener(dialogInterface -> cancel());

View File

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

View File

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

View File

@ -11,7 +11,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
*/
@Database(
entities = [ServerSetting::class],
version = 5,
version = 6,
exportSchema = true
)
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`")
}
}
/* ktlint-disable max-line-length */
val MIGRATION_5_6: Migration = object : Migration(5, 6) {
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 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
data class ServerSetting(
@ -32,14 +30,17 @@ data class ServerSetting(
@ColumnInfo(name = "password") var password: String,
@ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: 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 = "minimumApiVersion") var minimumApiVersion: String?,
@ColumnInfo(name = "chatSupport") var chatSupport: Boolean? = null,
@ColumnInfo(name = "bookmarkSupport") var bookmarkSupport: 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 (
0, 0, "", "", null, "", "", false, false, false, null, null
)

View File

@ -13,8 +13,11 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
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.FlagMode
import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener
import java.io.IOException
import java.net.MalformedURLException
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.androidx.viewmodel.ext.android.viewModel
import org.moire.ultrasonic.BuildConfig
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.ServerSetting
import org.moire.ultrasonic.model.EditServerModel
import org.moire.ultrasonic.model.ServerSettingsModel
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.InfoDialog
import org.moire.ultrasonic.util.ModalBackgroundTask
import org.moire.ultrasonic.util.ServerColor
import org.moire.ultrasonic.util.Util
import retrofit2.Response
import timber.log.Timber
private const val DIALOG_PADDING = 12
@ -78,6 +74,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
private var selectedColor: Int? = null
private val navArgs by navArgs<EditServerFragmentArgs>()
val model: EditServerModel by viewModels()
@Override
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
*/
@Suppress("TooGenericExceptionCaught")
private fun testConnection() {
val task: ModalBackgroundTask<String> = object : ModalBackgroundTask<String>(
activity,
false
) {
fun boolToMark(value: Boolean?): String {
if (value == null)
return ""
return if (value) "✔️" else ""
}
val testSetting = ServerSetting()
val builder = InfoDialog.Builder(requireContext())
builder.setTitle(R.string.supported_server_features)
builder.setMessage(getProgress(testSetting))
val dialog: AlertDialog = builder.create()
dialog.show()
fun getProgress(): String {
return String.format(
"""
val testJob = lifecycleScope.launch {
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_bookmarks)}
|%s - ${resources.getString(R.string.button_bar_shares)}
|%s - ${resources.getString(R.string.button_bar_podcasts)}
|%s - ${resources.getString(R.string.main_videos)}
|%s - ${resources.getString(R.string.jukebox)}
""".trimMargin(),
boolToMark(currentServerSetting!!.chatSupport),
boolToMark(currentServerSetting!!.bookmarkSupport),
boolToMark(currentServerSetting!!.shareSupport),
boolToMark(currentServerSetting!!.podcastSupport)
)
}
boolToMark(serverSetting.chatSupport),
boolToMark(serverSetting.bookmarkSupport),
boolToMark(serverSetting.shareSupport),
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)
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()
return progressString
}
private fun isServerFunctionAvailable(function: () -> Response<out SubsonicResponse>): Boolean {
return try {
function().falseOnFailure()
} catch (_: IOException) {
false
} catch (_: SubsonicRESTException) {
false
}
private fun boolToMark(value: Boolean?): String {
if (value == null)
return ""
return if (value) "✔️" else ""
}
/**
@ -522,7 +452,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler {
*/
private fun finishActivity() {
if (areFieldsChanged()) {
ErrorDialog.Builder(context)
ErrorDialog.Builder(requireContext())
.setTitle(R.string.common_confirm)
.setMessage(R.string.server_editor_leave_confirmation)
.setPositiveButton(R.string.common_ok) { dialog, _ ->

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.fragment
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.graphics.Canvas
import android.graphics.Color.argb
import android.graphics.Point
@ -90,6 +89,7 @@ import org.moire.ultrasonic.subsonic.NetworkAndStorageChecker
import org.moire.ultrasonic.subsonic.ShareHandler
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.CommunicationError
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.Util
import org.moire.ultrasonic.util.toTrack
@ -1256,12 +1256,14 @@ class PlayerFragment :
mediaPlayerController.setSongRating(rating)
}
@SuppressLint("InflateParams")
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)
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
val builder = ConfirmationDialog.Builder(requireContext())
builder.setTitle(R.string.download_playlist_title)
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
*/
private fun deleteServerById(id: Int) {
ErrorDialog.Builder(context)
// FIXME
ErrorDialog.Builder(requireContext())
.setTitle(R.string.server_menu_delete)
.setMessage(R.string.server_selector_delete_confirmation)
.setPositiveButton(R.string.common_delete) { dialog, _ ->

View File

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

View File

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

View File

@ -8,7 +8,6 @@
package org.moire.ultrasonic.fragment.legacy
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.os.Bundle
import android.text.Spannable
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.CacheCleaner
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.InfoDialog
import org.moire.ultrasonic.util.LoadingTask
import org.moire.ultrasonic.util.Util.applyTheme
import org.moire.ultrasonic.util.Util.toast
@ -222,7 +223,7 @@ class PlaylistsFragment : Fragment() {
}
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(
resources.getString(R.string.delete_playlist, playlist.name)
).setPositiveButton(R.string.common_ok) { _, _ ->
@ -283,8 +284,8 @@ class PlaylistsFragment : Fragment() {
Linkify.addLinks(message, Linkify.WEB_URLS)
textView.text = message
textView.movementMethod = LinkMovementMethod.getInstance()
AlertDialog.Builder(context).setTitle(playlist.name).setCancelable(true)
.setIcon(R.drawable.ic_baseline_info).setView(textView).show()
InfoDialog.Builder(requireContext()).setTitle(playlist.name).setCancelable(true)
.setView(textView).show()
}
@SuppressLint("InflateParams")
@ -301,7 +302,7 @@ class PlaylistsFragment : Fragment() {
} else {
publicBox.isChecked = pub
}
val alertDialog = AlertDialog.Builder(context)
val alertDialog = ConfirmationDialog.Builder(requireContext())
alertDialog.setIcon(R.drawable.ic_baseline_warning)
alertDialog.setTitle(R.string.playlist_update_info)
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
import android.app.AlertDialog
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
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.util.BackgroundTask
import org.moire.ultrasonic.util.CancellationToken
import org.moire.ultrasonic.util.ConfirmationDialog
import org.moire.ultrasonic.util.FragmentBackgroundTask
import org.moire.ultrasonic.util.Settings
import org.moire.ultrasonic.util.ShareDetails
@ -150,6 +151,8 @@ class ShareHandler(val context: Context) {
}
}
@Suppress("LongMethod")
@SuppressLint("InflateParams")
private fun showDialog(
fragment: Fragment,
shareDetails: ShareDetails,
@ -184,7 +187,7 @@ class ShareHandler(val context: Context) {
}
updateVisibility()
val builder = AlertDialog.Builder(fragment.context)
val builder = ConfirmationDialog.Builder(fragment.requireContext())
builder.setTitle(R.string.share_set_share_options)
builder.setPositiveButton(R.string.menu_share) { _, _ ->

View File

@ -8,10 +8,14 @@
package org.moire.ultrasonic.util
import android.app.Activity
import android.app.AlertDialog
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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(
context: Context,
message: CharSequence?,
@ -19,7 +23,7 @@ open class InfoDialog(
private val finishActivityOnClose: Boolean = false
) {
open var builder: AlertDialog.Builder = Builder(activity ?: context, message)
open var builder: MaterialAlertDialogBuilder = Builder(activity ?: context, message)
fun show() {
builder.setOnCancelListener {
@ -35,7 +39,7 @@ open class InfoDialog(
builder.create().show()
}
class Builder(context: Context?) : AlertDialog.Builder(context) {
class Builder(context: Context) : MaterialAlertDialogBuilder(context) {
constructor(context: Context, message: CharSequence?) : this(context) {
setMessage(message)
@ -44,7 +48,6 @@ open class InfoDialog(
init {
setIcon(R.drawable.ic_baseline_info)
setTitle(R.string.common_confirm)
setCancelable(true)
setPositiveButton(R.string.common_ok) { _, _ ->
// 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(
context: Context,
message: CharSequence?,
@ -59,9 +66,9 @@ class ErrorDialog(
finishActivityOnClose: Boolean = false
) : 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) {
setMessage(message)
}
@ -69,7 +76,6 @@ class ErrorDialog(
init {
setIcon(R.drawable.ic_baseline_warning)
setTitle(R.string.error_label)
setCancelable(true)
setPositiveButton(R.string.common_ok) { _, _ ->
// 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(
context: Context,
message: CharSequence?,
@ -84,9 +94,9 @@ class ConfirmationDialog(
finishActivityOnClose: Boolean = false
) : 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) {
setMessage(message)
}

View File

@ -157,7 +157,6 @@
<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_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_artists">Výchozí umělci</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_summary">Připojovat číslo skladby při zobrazování skladby</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_dark">Tmavý</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_summary">Lesezeichen nach Wiedergabe 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_artists">Anzahl der Künstler*innen</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_summary">Titel mit Nummer anzeigen</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_dark">Dunkel</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_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.connection_failure">Fallo de conexión.</string>
<string name="settings.default_albums">Álbumes predeterminados</string>
<string name="settings.default_artists">Artistas predeterminados</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_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.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_light">Claro</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_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.connection_failure">Échec de la connexion</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_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_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.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_dark">Sombre</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_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.connection_failure">Csatlakozási hiba!</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_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_summary">Dalok sorszámának megjeleníté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_dark">Sötét</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_summary">Pulisci segnalibro al completamento della riproduzione di una canzone</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_artists">Artisti predefiniti</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_summary">Includi numero traccia quando visualizzi una canzone</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_dark">Scuro</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_summary">Bladwijzer verwijderen nadat nummer is afgespeeld</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_artists">Standaardartiesten</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_summary">Itemnummer tonen tijdens tonen van nummers</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_light">Licht</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_summary">Czyść zakładkę po zakończeniu odtwarzania utworu</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_artists">Domyślna ilość wyników - artyści</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_summary">Dołącza numer utworu podczas wyświetlania utworu</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_dark">Ciemny</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_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.connection_failure">Falha na conexão.</string>
<string name="settings.default_albums">Álbuns Padrões</string>
<string name="settings.default_artists">Artistas 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_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.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_light">Claro</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_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.connection_failure">Falha na conexão.</string>
<string name="settings.default_albums">Álbuns Padrões</string>
<string name="settings.default_artists">Artistas 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_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.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_dark">Escuro</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_summary">Очистить закладку после завершения воспроизведения песни</string>
<string name="settings.clear_search_history">Очистить историю поиска</string>
<string name="settings.connection_failure">Ошибка подключения.</string>
<string name="settings.default_albums">Альбомы по умолчанию</string>
<string name="settings.default_artists">Исполнители по умолчанию</string>
<string name="settings.default_songs">Треки по умолчанию</string>
@ -274,8 +273,6 @@
<string name="settings.show_track_number">Показать номер трека</string>
<string name="settings.show_track_number_summary">Включить номер дорожки при отображении песни</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_dark">Темная</string>
<string name="settings.theme_black">Черная</string>

View File

@ -181,7 +181,6 @@
<string name="settings.clear_bookmark">清空书签</string>
<string name="settings.clear_bookmark_summary">歌曲播放完毕后清除书签</string>
<string name="settings.clear_search_history">清空搜索历史</string>
<string name="settings.connection_failure">连接失败</string>
<string name="settings.default_albums">默认专辑</string>
<string name="settings.default_artists">默认艺术家</string>
<string name="settings.default_songs">默认音乐</string>
@ -277,8 +276,6 @@
<string name="settings.show_track_number">显示曲目编号</string>
<string name="settings.show_track_number_summary">显示歌曲时包括曲目编号</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_dark">Dark</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_summary">Clear bookmark upon completion of playback of a song</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_artists">Default Artists</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_summary">Include track number when displaying a song</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_light">Light</string>
<string name="settings.theme_dark">Dark</string>
@ -446,11 +443,12 @@
<!-- Subsonic features -->
<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.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="list_view">List</string>
<string name="grid_view">Cover</string>
<string name="supported_server_features">Supported features</string>
<string name="jukebox">Jukebox</string>
</resources>