diff --git a/dependencies.gradle b/dependencies.gradle index 70e160bb..33107526 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,5 +1,5 @@ ext.versions = [ - minSdk : 14, + minSdk : 21, targetSdk : 29, compileSdk : 29, // You need to run ./gradlew wrapper after updating the version @@ -42,6 +42,7 @@ ext.versions = [ dexter : "6.2.3", timber : "4.7.1", fastScroll : "2.0.1", + colorPicker : "2.2.3", ] ext.gradlePlugins = [ @@ -89,6 +90,7 @@ ext.other = [ timber : "com.jakewharton.timber:timber:$versions.timber", fastScroll : "com.simplecityapps:recyclerview-fastscroll:$versions.fastScroll", sortListView : "com.github.tzugen:drag-sort-listview:$versions.sortListView", + colorPickerView : "com.github.skydoves:colorpickerview:$versions.colorPicker", ] ext.testing = [ diff --git a/ultrasonic/build.gradle b/ultrasonic/build.gradle index 4cc54f62..6c676aa5 100644 --- a/ultrasonic/build.gradle +++ b/ultrasonic/build.gradle @@ -105,6 +105,7 @@ dependencies { implementation other.okhttpLogging implementation other.fastScroll implementation other.sortListView + implementation other.colorPickerView kapt androidSupport.room 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 e8c05cc7..ddbb62fb 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/AppDatabase.kt @@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase * Room Database to be used to store global data for the whole app. * This could be settings or data that are not specific to any remote music database */ -@Database(entities = [ServerSetting::class], version = 3) +@Database(entities = [ServerSetting::class], version = 4) abstract class AppDatabase : RoomDatabase() { /** @@ -42,3 +42,11 @@ val MIGRATION_2_3: Migration = object : Migration(2, 3) { ) } } + +val MIGRATION_3_4: Migration = object : Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "ALTER TABLE ServerSetting ADD COLUMN color INTEGER" + ) + } +} 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 705f526a..e3fb9b04 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/data/ServerSetting.kt @@ -23,6 +23,7 @@ data class ServerSetting( @ColumnInfo(name = "index") var index: Int, @ColumnInfo(name = "name") var name: String, @ColumnInfo(name = "url") var url: String, + @ColumnInfo(name = "color") var color: Int? = null, @ColumnInfo(name = "userName") var userName: String, @ColumnInfo(name = "password") var password: String, @ColumnInfo(name = "jukeboxByDefault") var jukeboxByDefault: Boolean, @@ -36,9 +37,9 @@ data class ServerSetting( @ColumnInfo(name = "podcastSupport") var podcastSupport: Boolean? = null ) { constructor() : this ( - -1, 0, "", "", "", "", false, false, false, null, null + -1, 0, "", "", null, "", "", false, false, false, null, null ) constructor(name: String, url: String) : this( - -1, 0, name, url, "", "", false, false, false, null, null + -1, 0, name, url, null, "", "", false, false, false, null, null ) } diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt index 26b3df43..992141e1 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/di/AppPermanentStorageModule.kt @@ -8,6 +8,7 @@ import org.koin.dsl.module import org.moire.ultrasonic.data.AppDatabase import org.moire.ultrasonic.data.MIGRATION_1_2 import org.moire.ultrasonic.data.MIGRATION_2_3 +import org.moire.ultrasonic.data.MIGRATION_3_4 import org.moire.ultrasonic.fragment.ServerSettingsModel import org.moire.ultrasonic.util.Settings @@ -28,6 +29,7 @@ val appPermanentStorage = module { ) .addMigrations(MIGRATION_1_2) .addMigrations(MIGRATION_2_3) + .addMigrations(MIGRATION_3_4) .build() } 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 077a7587..c0bd3191 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/EditServerFragment.kt @@ -6,15 +6,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.ImageView +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.textfield.TextInputLayout -import java.io.IOException -import java.net.MalformedURLException -import java.net.URL -import java.util.Locale +import com.skydoves.colorpickerview.ColorPickerDialog +import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.moire.ultrasonic.BuildConfig @@ -35,6 +35,15 @@ import org.moire.ultrasonic.util.ModalBackgroundTask import org.moire.ultrasonic.util.Util import retrofit2.Response import timber.log.Timber +import java.io.IOException +import java.net.MalformedURLException +import java.net.URL +import java.util.* +import com.skydoves.colorpickerview.flag.FlagMode + +import com.skydoves.colorpickerview.flag.BubbleFlag +import org.moire.ultrasonic.util.ServerColor + /** * Displays a form where server settings can be created / edited @@ -51,6 +60,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { private var serverNameEditText: TextInputLayout? = null private var serverAddressEditText: TextInputLayout? = null + private var serverColorImageView: ImageView? = null private var userNameEditText: TextInputLayout? = null private var passwordEditText: TextInputLayout? = null private var selfSignedSwitch: SwitchMaterial? = null @@ -59,6 +69,8 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { private var saveButton: Button? = null private var testButton: Button? = null private var isInstanceStateSaved: Boolean = false + private var currentColor: Int = 0 + private var selectedColor: Int? = null @Override override fun onCreate(savedInstanceState: Bundle?) { @@ -79,6 +91,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { serverNameEditText = view.findViewById(R.id.edit_server_name) serverAddressEditText = view.findViewById(R.id.edit_server_address) + serverColorImageView = view.findViewById(R.id.edit_server_color_picker) userNameEditText = view.findViewById(R.id.edit_server_username) passwordEditText = view.findViewById(R.id.edit_server_password) selfSignedSwitch = view.findViewById(R.id.edit_self_signed) @@ -148,6 +161,32 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { testConnection() } } + + serverColorImageView!!.setOnClickListener { + val bubbleFlag = BubbleFlag(context) + bubbleFlag.flagMode = FlagMode.LAST + ColorPickerDialog.Builder(context).apply { + this.colorPickerView.setInitialColor(currentColor) + this.colorPickerView.setFlagView(bubbleFlag) + } + .attachAlphaSlideBar(false) + .setPositiveButton(getString(R.string.common_ok), + ColorEnvelopeListener { envelope, _ -> + selectedColor = envelope.color + updateColor(envelope.color) }) + .setNegativeButton(getString(R.string.common_cancel)) { + dialogInterface, i -> dialogInterface.dismiss() + } + .setBottomSpace(12) + .show() + } + } + + private fun updateColor(color: Int?) { + val image = ContextCompat.getDrawable(requireContext(), R.drawable.thumb_drawable) + currentColor = ServerColor.getBackgroundColor(requireContext(), color) + image?.setTint(currentColor) + serverColorImageView?.background = image } override fun onBackPressed() { @@ -176,6 +215,13 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { savedInstanceState.putBoolean( ::jukeboxSwitch.name, jukeboxSwitch!!.isChecked ) + savedInstanceState.putInt( + ::serverColorImageView.name, currentColor + ) + if (selectedColor != null) + savedInstanceState.putInt( + ::selectedColor.name, selectedColor!! + ) savedInstanceState.putBoolean( ::isInstanceStateSaved.name, true ) @@ -203,6 +249,9 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { selfSignedSwitch!!.isChecked = savedInstanceState.getBoolean(::selfSignedSwitch.name) ldapSwitch!!.isChecked = savedInstanceState.getBoolean(::ldapSwitch.name) jukeboxSwitch!!.isChecked = savedInstanceState.getBoolean(::jukeboxSwitch.name) + updateColor(savedInstanceState.getInt(::serverColorImageView.name)) + if (savedInstanceState.containsKey(::selectedColor.name)) + selectedColor = savedInstanceState.getInt(::selectedColor.name) isInstanceStateSaved = savedInstanceState.getBoolean(::isInstanceStateSaved.name) } @@ -219,6 +268,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { selfSignedSwitch!!.isChecked = currentServerSetting!!.allowSelfSignedCertificate ldapSwitch!!.isChecked = currentServerSetting!!.ldapSupport jukeboxSwitch!!.isChecked = currentServerSetting!!.jukeboxByDefault + updateColor(currentServerSetting!!.color) } /** @@ -267,6 +317,7 @@ class EditServerFragment : Fragment(), OnBackPressedHandler { if (isValid) { currentServerSetting!!.name = serverNameEditText!!.editText?.text.toString() currentServerSetting!!.url = serverAddressEditText!!.editText?.text.toString() + currentServerSetting!!.color = selectedColor currentServerSetting!!.userName = userNameEditText!!.editText?.text.toString() currentServerSetting!!.password = passwordEditText!!.editText?.text.toString() currentServerSetting!!.allowSelfSignedCertificate = selfSignedSwitch!!.isChecked diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt index 5bc27b0c..1f9e4c42 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerRowAdapter.kt @@ -14,10 +14,12 @@ import android.widget.ImageView import android.widget.PopupMenu import android.widget.RelativeLayout import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import org.moire.ultrasonic.R import org.moire.ultrasonic.data.ActiveServerProvider import org.moire.ultrasonic.data.ServerSetting +import org.moire.ultrasonic.util.ServerColor import org.moire.ultrasonic.util.Util /** @@ -77,15 +79,15 @@ internal class ServerRowAdapter( val text = vi?.findViewById(R.id.server_name) val description = vi?.findViewById(R.id.server_description) - val layout = vi?.findViewById(R.id.server_layout) + val layout = vi?.findViewById(R.id.server_layout) val image = vi?.findViewById(R.id.server_image) val serverMenu = vi?.findViewById(R.id.server_menu) + val setting = data.singleOrNull { t -> t.index == index } if (index == 0) { text?.text = context.getString(R.string.main_offline) description?.text = "" } else { - val setting = data.singleOrNull { t -> t.index == index } text?.text = setting?.name ?: "" description?.text = setting?.url ?: "" if (setting == null) serverMenu?.visibility = View.INVISIBLE @@ -95,8 +97,14 @@ internal class ServerRowAdapter( if (index == 0) { serverMenu?.visibility = View.INVISIBLE image?.setImageDrawable(Util.getDrawableFromAttribute(context, R.attr.screen_on_off)) + image?.background = ContextCompat.getDrawable(context, R.color.transparent) } else { - image?.setImageDrawable(Util.getDrawableFromAttribute(context, R.attr.server)) + val icon = ContextCompat.getDrawable(context, R.drawable.ic_menu_server_dark) + icon?.setTint(ServerColor.getForegroundColor(context, setting?.color)) + image?.setImageDrawable(icon) + val background = ContextCompat.getDrawable(context, R.drawable.circle) + background?.setTint(ServerColor.getBackgroundColor(context, setting?.color)) + image?.background = background } // Highlight the Active Server's row by changing its background diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt index ad8499bf..65c2c6e6 100644 --- a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/fragment/ServerSettingsModel.kt @@ -212,6 +212,7 @@ class ServerSettingsModel( serverId, settings.getString(PREFERENCES_KEY_SERVER_NAME + preferenceId, "")!!, url, + null, userName, settings.getString(PREFERENCES_KEY_PASSWORD + preferenceId, "")!!, settings.getBoolean(PREFERENCES_KEY_JUKEBOX_BY_DEFAULT + preferenceId, false), diff --git a/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ServerColor.kt b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ServerColor.kt new file mode 100644 index 00000000..9f34d02a --- /dev/null +++ b/ultrasonic/src/main/kotlin/org/moire/ultrasonic/util/ServerColor.kt @@ -0,0 +1,26 @@ +package org.moire.ultrasonic.util + +import android.content.Context +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import org.moire.ultrasonic.R + +object ServerColor { + fun getBackgroundColor(context: Context, serverColor: Int?): Int { + return serverColor ?: ContextCompat.getColor( + context, Util.getResourceFromAttribute(context, R.attr.colorPrimary) + ) + } + + fun getForegroundColor(context: Context, serverColor: Int?): Int { + if (serverColor == null) return ContextCompat.getColor( + context, Util.getResourceFromAttribute(context, R.attr.colorOnPrimary) + ) + val luminance = ColorUtils.calculateLuminance(serverColor) + return if (luminance < 0.25) { + ContextCompat.getColor(context, R.color.selected_menu_dark) + } else { + ContextCompat.getColor(context, R.color.selected_menu_light) + } + } +} \ No newline at end of file diff --git a/ultrasonic/src/main/res/drawable/circle.xml b/ultrasonic/src/main/res/drawable/circle.xml new file mode 100644 index 00000000..f42d2b06 --- /dev/null +++ b/ultrasonic/src/main/res/drawable/circle.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/drawable/rounded_border.xml b/ultrasonic/src/main/res/drawable/rounded_border.xml new file mode 100644 index 00000000..5c60efee --- /dev/null +++ b/ultrasonic/src/main/res/drawable/rounded_border.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/ultrasonic/src/main/res/layout/server_edit.xml b/ultrasonic/src/main/res/layout/server_edit.xml index 1058c3f6..18e1c923 100644 --- a/ultrasonic/src/main/res/layout/server_edit.xml +++ b/ultrasonic/src/main/res/layout/server_edit.xml @@ -14,7 +14,6 @@ a:layout_width="match_parent" a:layout_height="wrap_content" a:hint="@string/settings.server_name" - app:layout_constraintBottom_toTopOf="@id/edit_server_address" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -33,7 +32,6 @@ a:layout_height="wrap_content" a:layout_marginBottom="20dp" a:hint="@string/settings.server_address" - app:layout_constraintBottom_toTopOf="@id/edit_authentication_header" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/edit_server_name"> @@ -46,15 +44,38 @@ + + + + + app:layout_constraintTop_toBottomOf="@id/edit_server_color_picker" /> - + a:layout_margin="10dp" + a:background="@drawable/circle" + a:focusable="false" + a:paddingHorizontal="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/ic_menu_server_dark" /> + app:layout_constraintStart_toEndOf="@id/server_image" + app:layout_constraintTop_toTopOf="@id/server_image" + app:layout_constraintEnd_toStartOf="@id/server_menu" + tools:text="Server name" /> + app:layout_constraintBottom_toBottomOf="@id/server_image" + app:layout_constraintStart_toEndOf="@id/server_image" + app:layout_constraintTop_toBottomOf="@id/server_name" + app:layout_constraintEnd_toStartOf="@id/server_menu" + tools:text="Server description" /> - + diff --git a/ultrasonic/src/main/res/values/strings.xml b/ultrasonic/src/main/res/values/strings.xml index 86687bc1..9ce97deb 100644 --- a/ultrasonic/src/main/res/values/strings.xml +++ b/ultrasonic/src/main/res/values/strings.xml @@ -295,6 +295,7 @@ Server-Side Album Art Scaling Unused Username + Server color Show Lock Screen Controls Show playback controls on the lock screen Show Notification