diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index f3a4f182..706c1dbc 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,7 +1,7 @@ diff --git a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt index 858632e2..c6a7559a 100644 --- a/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt +++ b/core/subsonic-api/src/main/kotlin/org/moire/ultrasonic/api/subsonic/SubsonicAPIDefinition.kt @@ -54,6 +54,9 @@ interface SubsonicAPIDefinition { @GET("ping.view") fun ping(): Call + @GET("ping.view") + suspend fun pingSuspend(): SubsonicResponse + @GET("getLicense.view") fun getLicense(): Call @@ -164,6 +167,12 @@ interface SubsonicAPIDefinition { @Query("id") id: String? = null ): Call + @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 + @GET("getShares.view") + suspend fun getSharesSuspend(): SharesResponse + @GET("createShare.view") fun createShare( @Query("id") idsToShare: List, @@ -292,15 +304,24 @@ interface SubsonicAPIDefinition { @GET("getUser.view") fun getUser(@Query("username") username: String): Call + @GET("getUser.view") + suspend fun getUserSuspend(@Query("username") username: String): GetUserResponse + @GET("getChatMessages.view") fun getChatMessages(@Query("since") since: Long? = null): Call + @GET("getChatMessages.view") + suspend fun getChatMessagesSuspend(@Query("since") since: Long? = null): ChatMessagesResponse + @GET("addChatMessage.view") fun addChatMessage(@Query("message") message: String): Call @GET("getBookmarks.view") fun getBookmarks(): Call + @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 + @GET("getVideos.view") + suspend fun getVideosSuspend(): VideosResponse + @GET("getAvatar.view") fun getAvatar(@Query("username") username: String): Call } diff --git a/detekt-config.yml b/detekt-config.yml index 0153fd2e..ef02adeb 100644 --- a/detekt-config.yml +++ b/detekt-config.yml @@ -66,6 +66,8 @@ style: active: false ReturnCount: max: 5 + ForbiddenImport: + imports: ['android.app.AlertDialog'] comments: active: true diff --git a/ultrasonic/schemas/org.moire.ultrasonic.data.AppDatabase/6.json b/ultrasonic/schemas/org.moire.ultrasonic.data.AppDatabase/6.json new file mode 100644 index 00000000..bb93145e --- /dev/null +++ b/ultrasonic/schemas/org.moire.ultrasonic.data.AppDatabase/6.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java index bdf37539..6a091f72 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/fragment/ChatFragment.java @@ -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; diff --git a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ModalBackgroundTask.java b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ModalBackgroundTask.java index 18c3bfa5..1a963027 100644 --- a/ultrasonic/src/main/java/org/moire/ultrasonic/util/ModalBackgroundTask.java +++ b/ultrasonic/src/main/java/org/moire/ultrasonic/util/ModalBackgroundTask.java @@ -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 extends BackgroundTask 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()); diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt index 53e80438..61c1a1aa 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/activity/NavigationActivity.kt @@ -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 } } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt index 4b94c4ba..04ff6b8e 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ActiveServerProvider.kt @@ -214,7 +214,9 @@ class ActiveServerProvider( bookmarkSupport = false, podcastSupport = false, shareSupport = false, - chatSupport = false + chatSupport = false, + videoSupport = false, + jukeboxSupport = false ) /** diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt index a5d1aa8c..ef23f36f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -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) { diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt index 3b212fec..a4a374c4 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt @@ -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 ) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt index 0da63dbb..e16e0d53 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -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() + 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 = object : ModalBackgroundTask( - 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): 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, _ -> diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt index 3ef77e13..a8f4f19c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/PlayerFragment.kt @@ -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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt index bb5a9227..5f79b53f 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSelectorFragment.kt @@ -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, _ -> diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt index 016765c1..4c25e30c 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/SettingsFragment.kt @@ -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() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt index e8e6f477..535bfb59 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/TrackCollectionFragment.kt @@ -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() diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt index 40fd218b..ff2f8a73 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/legacy/PlaylistsFragment.kt @@ -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) diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt new file mode 100644 index 00000000..24a876df --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/model/EditServerModel.kt @@ -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 { + 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) + } +} diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt index 64b9c4ec..c9855d89 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/subsonic/ShareHandler.kt @@ -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) { _, _ -> diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt index e59937d9..a33dae82 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/Dialogs.kt @@ -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) } diff --git a/ultrasonic/src/main/res/values-cs/strings.xml b/ultrasonic/src/main/res/values-cs/strings.xml index 840e01ca..10634765 100644 --- a/ultrasonic/src/main/res/values-cs/strings.xml +++ b/ultrasonic/src/main/res/values-cs/strings.xml @@ -157,7 +157,6 @@ Zahodit záložku Zahodit záložku po dokončení přehrávání skladby Vyčistit historii vyhledávání - Chyba připojení. Výchozí alba Výchozí umělci Výchozí skladby @@ -248,8 +247,6 @@ Zobrazovat číslo skladby Připojovat číslo skladby při zobrazování skladby Test připojení - Připojení je v pořádku - Připojení je v pořádku. Server bez licence. Světlý Tmavý Černý diff --git a/ultrasonic/src/main/res/values-de/strings.xml b/ultrasonic/src/main/res/values-de/strings.xml index 204e894c..07d2f06a 100644 --- a/ultrasonic/src/main/res/values-de/strings.xml +++ b/ultrasonic/src/main/res/values-de/strings.xml @@ -196,7 +196,6 @@ Lesezeichen löschen Lesezeichen nach Wiedergabe löschen Suchverlauf löschen - Verbindungsfehler Anzahl der Alben Anzahl der Künstler*innen Anzahl der Titel @@ -297,8 +296,6 @@ Titelnummer anzeigen Titel mit Nummer anzeigen Verbindung testen - Verbindung OK - Verbindung OK, Server nicht lizenziert. Hell Dunkel Schwarz diff --git a/ultrasonic/src/main/res/values-es/strings.xml b/ultrasonic/src/main/res/values-es/strings.xml index d0eeb6fc..cb8c09d0 100644 --- a/ultrasonic/src/main/res/values-es/strings.xml +++ b/ultrasonic/src/main/res/values-es/strings.xml @@ -198,7 +198,6 @@ Limpiar marcador Limpiar marcador tras la finalización de la reproducción de una canción Limpiar el historial de búsqueda - Fallo de conexión. Álbumes predeterminados Artistas predeterminados Canciones predeterminadas @@ -299,8 +298,6 @@ Mostrar número de pista Incluir el número de pista cuando se muestre una canción Comprobar conexión - La conexión es correcta - La conexión es correcta. Servidor sin licencia. Día y noche Claro Oscuro diff --git a/ultrasonic/src/main/res/values-fr/strings.xml b/ultrasonic/src/main/res/values-fr/strings.xml index 90b464c2..437e10db 100644 --- a/ultrasonic/src/main/res/values-fr/strings.xml +++ b/ultrasonic/src/main/res/values-fr/strings.xml @@ -194,7 +194,6 @@ Effacer le signet Effacer le signet à la fin de la lecture d\'un titre Effacer l\'historique des recherches - Échec de la connexion Albums par défaut Artistes par défaut Musiques par défaut @@ -291,8 +290,6 @@ Afficher le numéro du titre Inclure son numéro lors de l\'affichage d\'un titre Tester la connexion - Connexion correcte - Connexion correcte. Serveur sans licence. Clair Sombre Noir diff --git a/ultrasonic/src/main/res/values-hu/strings.xml b/ultrasonic/src/main/res/values-hu/strings.xml index 8e7fe5f6..402514bf 100644 --- a/ultrasonic/src/main/res/values-hu/strings.xml +++ b/ultrasonic/src/main/res/values-hu/strings.xml @@ -163,7 +163,6 @@ Könyvjelző törlése Könyvjelző törlése a dal lejátszása után. Keresési előzmények törlése - Csatlakozási hiba! Albumok találati száma Előadók találati száma Dalok találati száma @@ -256,8 +255,6 @@ Sorszám megjelenítése Dalok sorszámának megjelenítése. Kapcsolat tesztelése - Kapcsolat OK! - Kapcsolat OK! A kiszolgálónak nincs licence! Világos Sötét Fekete diff --git a/ultrasonic/src/main/res/values-it/strings.xml b/ultrasonic/src/main/res/values-it/strings.xml index c21a3ba7..02686cde 100644 --- a/ultrasonic/src/main/res/values-it/strings.xml +++ b/ultrasonic/src/main/res/values-it/strings.xml @@ -154,7 +154,6 @@ Pulisci Segnalibro Pulisci segnalibro al completamento della riproduzione di una canzone Pulisci Storico Ricerca - Errore connessione. Album predefiniti Artisti predefiniti Canzoni predefinte @@ -240,8 +239,6 @@ Visualizza numero traccia Includi numero traccia quando visualizzi una canzone Prova Connessione - Connessione OK - Connessione OK. Server senza licenza. Chiaro Scuro Tema diff --git a/ultrasonic/src/main/res/values-nl/strings.xml b/ultrasonic/src/main/res/values-nl/strings.xml index 4a1aaded..86744704 100644 --- a/ultrasonic/src/main/res/values-nl/strings.xml +++ b/ultrasonic/src/main/res/values-nl/strings.xml @@ -198,7 +198,6 @@ Bladwijzer verwijderen Bladwijzer verwijderen nadat nummer is afgespeeld Zoekgeschiedenis wissen - Verbindingsfout. Standaardalbums Standaardartiesten Standaardnummers @@ -299,8 +298,6 @@ Itemnummer tonen Itemnummer tonen tijdens tonen van nummers Verbinding testen - Verbinding is goed - Verbinding is goed; geen serverlicentie. Dag en nacht Licht Donker diff --git a/ultrasonic/src/main/res/values-pl/strings.xml b/ultrasonic/src/main/res/values-pl/strings.xml index 6adf7531..efff621c 100644 --- a/ultrasonic/src/main/res/values-pl/strings.xml +++ b/ultrasonic/src/main/res/values-pl/strings.xml @@ -157,7 +157,6 @@ Czyszczenie zakładek Czyść zakładkę po zakończeniu odtwarzania utworu Wyczyść historię wyszukiwania - Błąd połączenia. Domyślna ilość wyników - albumy Domyślna ilość wyników - artyści Domyślna ilość wyników - utwory @@ -248,8 +247,6 @@ Wyświetlaj numer utworu Dołącza numer utworu podczas wyświetlania utworu Testuj połączenie - Połączenie jest OK - Połączenie jest OK. Brak licencji na serwerze. Jasny Ciemny Motyw diff --git a/ultrasonic/src/main/res/values-pt-rBR/strings.xml b/ultrasonic/src/main/res/values-pt-rBR/strings.xml index ddfc4aa7..a4f2dc9a 100644 --- a/ultrasonic/src/main/res/values-pt-rBR/strings.xml +++ b/ultrasonic/src/main/res/values-pt-rBR/strings.xml @@ -196,7 +196,6 @@ Limpar Favoritos Limpar favoritos após terminar de tocar a música Limpar Histórico de Pesquisas - Falha na conexão. Álbuns Padrões Artistas Padrões Músicas Padrões @@ -297,8 +296,6 @@ Mostrar o Número da Faixa Incluir o número da faixa ao mostrar uma música Teste de Conexão - Conexão OK - Conexão OK. Servidor não licenciado. Dia & Noite Claro Escuro diff --git a/ultrasonic/src/main/res/values-pt/strings.xml b/ultrasonic/src/main/res/values-pt/strings.xml index 032a1c50..e188bfc7 100644 --- a/ultrasonic/src/main/res/values-pt/strings.xml +++ b/ultrasonic/src/main/res/values-pt/strings.xml @@ -157,7 +157,6 @@ Limpar Favoritos Limpar favoritos após terminar de tocar a música Limpar Histórico de Pesquisas - Falha na conexão. Álbuns Padrões Artistas Padrões Músicas Padrões @@ -248,8 +247,6 @@ Mostrar o Número da Faixa Incluir o número da faixa quando mostrando uma música Teste de Conexão - Conexão OK - Conexão OK. Servidor não licenciado. Claro Escuro Tema diff --git a/ultrasonic/src/main/res/values-ru/strings.xml b/ultrasonic/src/main/res/values-ru/strings.xml index fe19bc63..699fa66d 100644 --- a/ultrasonic/src/main/res/values-ru/strings.xml +++ b/ultrasonic/src/main/res/values-ru/strings.xml @@ -181,7 +181,6 @@ Очистить закладку Очистить закладку после завершения воспроизведения песни Очистить историю поиска - Ошибка подключения. Альбомы по умолчанию Исполнители по умолчанию Треки по умолчанию @@ -274,8 +273,6 @@ Показать номер трека Включить номер дорожки при отображении песни Тестовое соединение - Успешное соединение - Успешное соединение. Сервер нелицензионный. Светлая Темная Черная diff --git a/ultrasonic/src/main/res/values-zh-rCN/strings.xml b/ultrasonic/src/main/res/values-zh-rCN/strings.xml index 8cde6fcf..2419a85b 100644 --- a/ultrasonic/src/main/res/values-zh-rCN/strings.xml +++ b/ultrasonic/src/main/res/values-zh-rCN/strings.xml @@ -181,7 +181,6 @@ 清空书签 歌曲播放完毕后清除书签 清空搜索历史 - 连接失败 默认专辑 默认艺术家 默认音乐 @@ -277,8 +276,6 @@ 显示曲目编号 显示歌曲时包括曲目编号 测试连接 - 连接正常 - 连接正常, 服务器未授权。 Light Dark Black diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index f4961f15..b5ebfaa7 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -198,7 +198,6 @@ Clear Bookmark Clear bookmark upon completion of playback of a song Clear Search History - Connection failure. Default Albums Default Artists Default Songs @@ -299,8 +298,6 @@ Show Track Number Include track number when displaying a song Test Connection - Connection is OK - Connection is OK. Server unlicensed. Day & Night Light Dark @@ -446,11 +443,12 @@ Use five star rating for songs Use five star rating system for songs instead of simply starring/unstarring items. - Use hardware playback (experimental) Try to play the media using the media decoder chip on your phone. This can improve battery usage. List Cover + Supported features + Jukebox