feat(ui): option for automatic update checks (#5668)

This commit is contained in:
IndusAryan 2024-02-28 18:08:03 +05:30 committed by GitHub
parent 8ffa79ffcc
commit 5532301aa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 119 additions and 65 deletions

View File

@ -18,7 +18,7 @@ interface ExternalApi {
// fetch latest version info // fetch latest version info
@GET(GITHUB_API_URL) @GET(GITHUB_API_URL)
suspend fun getUpdateInfo(): UpdateInfo suspend fun getLatestRelease(): UpdateInfo
@POST("$SB_API_URL/api/skipSegments") @POST("$SB_API_URL/api/skipSegments")
suspend fun submitSegment( suspend fun submitSegment(

View File

@ -22,7 +22,8 @@ object IntentData {
const val shareData = "shareData" const val shareData = "shareData"
const val currentPosition = "currentPosition" const val currentPosition = "currentPosition"
const val duration = "duration" const val duration = "duration"
const val updateInfo = "updateInfo" const val appUpdateChangelog = "updateChangelog"
const val appUpdateURL = "updateURL"
const val backupFile = "backupFile" const val backupFile = "backupFile"
const val playlistTask = "playlistTask" const val playlistTask = "playlistTask"
const val loginTask = "loginTask" const val loginTask = "loginTask"

View File

@ -134,6 +134,7 @@ object PreferenceKeys {
/** /**
* Advanced * Advanced
*/ */
const val AUTOMATIC_UPDATE_CHECKS = "automatic_update_checks"
const val DATA_SAVER_MODE = "data_saver_mode_key" const val DATA_SAVER_MODE = "data_saver_mode_key"
const val MAX_IMAGE_CACHE = "image_cache_size" const val MAX_IMAGE_CACHE = "image_cache_size"
const val RESET_SETTINGS = "reset_settings" const val RESET_SETTINGS = "reset_settings"

View File

@ -8,6 +8,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
@Parcelize @Parcelize
data class UpdateInfo( data class UpdateInfo(
val name: String,
val body: String,
@SerialName("html_url") val htmlUrl: String, @SerialName("html_url") val htmlUrl: String,
val name: String
) : Parcelable ) : Parcelable

View File

@ -18,6 +18,7 @@ import androidx.core.view.allViews
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.findNavController import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
@ -44,6 +45,9 @@ import com.github.libretube.ui.fragments.PlayerFragment
import com.github.libretube.ui.models.PlayerViewModel import com.github.libretube.ui.models.PlayerViewModel
import com.github.libretube.ui.models.SearchViewModel import com.github.libretube.ui.models.SearchViewModel
import com.github.libretube.ui.models.SubscriptionsViewModel import com.github.libretube.ui.models.SubscriptionsViewModel
import com.github.libretube.util.UpdateChecker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : BaseActivity() { class MainActivity : BaseActivity() {
lateinit var binding: ActivityMainBinding lateinit var binding: ActivityMainBinding
@ -82,6 +86,13 @@ class MainActivity : BaseActivity() {
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
// Check update automatically
if (PreferenceHelper.getBoolean(PreferenceKeys.AUTOMATIC_UPDATE_CHECKS, false)) {
lifecycleScope.launch(Dispatchers.IO) {
UpdateChecker(this@MainActivity).checkUpdate(false)
}
}
// set the action bar for the activity // set the action bar for the activity
setSupportActionBar(binding.toolbar) setSupportActionBar(binding.toolbar)

View File

@ -6,28 +6,33 @@ import android.os.Bundle
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData.appUpdateChangelog
import com.github.libretube.extensions.parcelable import com.github.libretube.constants.IntentData.appUpdateURL
import com.github.libretube.obj.update.UpdateInfo
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
class UpdateAvailableDialog : DialogFragment() { class UpdateAvailableDialog : DialogFragment() {
private lateinit var updateInfo: UpdateInfo private var changelog: String? = null
private var releaseUrl: String? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
updateInfo = requireArguments().parcelable(IntentData.updateInfo)!! arguments?.run {
changelog = getString(appUpdateChangelog)
releaseUrl = getString(appUpdateURL)
}
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext()) return MaterialAlertDialogBuilder(requireContext())
.setTitle(context?.getString(R.string.update_available, updateInfo.name)) .setTitle(R.string.update_available)
.setMessage(context?.getString(R.string.update_available_text)) .setMessage(changelog)
.setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.download) { _, _ ->
.setPositiveButton(context?.getString(R.string.okay)) { _, _ -> releaseUrl?.let {
val intent = Intent(Intent.ACTION_VIEW).setData(updateInfo.htmlUrl.toUri()) startActivity(Intent(Intent.ACTION_VIEW, it.toUri()))
startActivity(intent)
} }
}
.setNegativeButton(R.string.tooltip_dismiss, null)
.setCancelable(false)
.show() .show()
} }
} }

View File

@ -1,21 +1,14 @@
package com.github.libretube.ui.preferences package com.github.libretube.ui.preferences
import android.os.Bundle import android.os.Bundle
import androidx.annotation.StringRes
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import com.github.libretube.BuildConfig import com.github.libretube.BuildConfig
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData
import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.base.BasePreferenceFragment import com.github.libretube.ui.base.BasePreferenceFragment
import com.github.libretube.ui.dialogs.UpdateAvailableDialog import com.github.libretube.util.UpdateChecker
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class MainSettings : BasePreferenceFragment() { class MainSettings : BasePreferenceFragment() {
override val titleResourceId: Int = R.string.settings override val titleResourceId: Int = R.string.settings
@ -24,50 +17,16 @@ class MainSettings : BasePreferenceFragment() {
setPreferencesFromResource(R.xml.settings, rootKey) setPreferencesFromResource(R.xml.settings, rootKey)
val update = findPreference<Preference>("update") val update = findPreference<Preference>("update")
update?.summary = "v${BuildConfig.VERSION_NAME}"
// set the version of the update preference // check app update manually
val versionString = if (BuildConfig.DEBUG) {
"${BuildConfig.VERSION_NAME} Debug"
} else {
getString(R.string.version, BuildConfig.VERSION_NAME)
}
update?.title = versionString
// checking for update: yes -> dialog, no -> snackBar
update?.setOnPreferenceClickListener { update?.setOnPreferenceClickListener {
lifecycleScope.launch {
// check for update lifecycleScope.launch(Dispatchers.IO) {
val updateInfo = try { UpdateChecker(requireContext()).checkUpdate(true)
withContext(Dispatchers.IO) {
RetrofitInstance.externalApi.getUpdateInfo()
}
} catch (e: Exception) {
showSnackBar(R.string.unknown_error)
return@launch
} }
if (BuildConfig.VERSION_NAME != updateInfo.name) {
// show the UpdateAvailableDialog if there's an update available
val newUpdateAvailableDialog = UpdateAvailableDialog()
newUpdateAvailableDialog.arguments =
bundleOf(IntentData.updateInfo to updateInfo)
newUpdateAvailableDialog.show(
childFragmentManager,
UpdateAvailableDialog::class.java.name
)
} else {
// otherwise show the no update available snackBar
showSnackBar(R.string.app_uptodate)
}
}
true true
} }
} }
private fun showSnackBar(@StringRes text: Int) {
(activity as? SettingsActivity)?.binding?.let {
Snackbar.make(it.root, text, Snackbar.LENGTH_SHORT)
.show()
}
}
} }

View File

@ -0,0 +1,70 @@
package com.github.libretube.util
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.FragmentActivity
import com.github.libretube.BuildConfig
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.IntentData.appUpdateChangelog
import com.github.libretube.constants.IntentData.appUpdateURL
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.ui.dialogs.UpdateAvailableDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.Locale
class UpdateChecker(private val context: Context) {
suspend fun checkUpdate(isManualCheck: Boolean = false) {
val currentAppVersion = BuildConfig.VERSION_NAME.replace(".", "").toInt()
try {
val response = RetrofitInstance.externalApi.getLatestRelease()
// version would be in the format "0.21.1"
val update = response.name.replace(".", "").toIntOrNull()
if (update != null && currentAppVersion < update) {
withContext(Dispatchers.Main) {
showUpdateAvailableDialog(response.body, response.htmlUrl)
}
Log.i(TAG(), response.toString())
} else if (isManualCheck) {
context.toastFromMainDispatcher(R.string.app_uptodate)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun showUpdateAvailableDialog(
changelog: String,
url: String,
) {
val dialog = UpdateAvailableDialog()
val args =
Bundle().apply {
putString(appUpdateChangelog, sanitizeChangelog(changelog))
putString(appUpdateURL, url)
}
dialog.arguments = args
val fragmentManager = (context as? FragmentActivity)?.supportFragmentManager
fragmentManager?.let {
dialog.show(it, UpdateAvailableDialog::class.java.simpleName)
}
}
private fun sanitizeChangelog(changelog: String): String {
val removeBloat = changelog.substringBeforeLast("**Full Changelog**")
val removeLinks = removeBloat.replace(Regex("in https://github\\.com/\\S+"), "")
val uppercaseChangeType =
removeLinks.lines().joinToString("\n") { line ->
if (line.startsWith("##")) line.uppercase(Locale.ROOT) + " :" else line
}
val removeHashes = uppercaseChangeType.replace("## ", "")
val cleanPrefix = removeHashes.replace("*", "")
return cleanPrefix.trim()
}
}

View File

@ -122,7 +122,7 @@
<string name="piped">Piped</string> <string name="piped">Piped</string>
<string name="youtube">YouTube</string> <string name="youtube">YouTube</string>
<string name="playOnBackground">Play in the background</string> <string name="playOnBackground">Play in the background</string>
<string name="update_available">Version %1$s is available</string> <string name="update_available">Update available</string>
<string name="update_available_text">New update available. Click to open the GitHub releases page.</string> <string name="update_available_text">New update available. Click to open the GitHub releases page.</string>
<string name="appearance">Appearance</string> <string name="appearance">Appearance</string>
<string name="downloads">Downloads</string> <string name="downloads">Downloads</string>
@ -154,6 +154,7 @@
<string name="clear_customInstances">Clear added</string> <string name="clear_customInstances">Clear added</string>
<string name="invalid_url">Please enter a URL that works</string> <string name="invalid_url">Please enter a URL that works</string>
<string name="version">Version %1$s</string> <string name="version">Version %1$s</string>
<string name="show_updates">Check updates automatically</string>
<string name="related_streams">Related content</string> <string name="related_streams">Related content</string>
<string name="related_streams_summary">Show similar streams alongside what you watch</string> <string name="related_streams_summary">Show similar streams alongside what you watch</string>
<string name="buffering_goal">Preloading</string> <string name="buffering_goal">Preloading</string>

View File

@ -4,6 +4,12 @@
<PreferenceCategory app:title="@string/advanced"> <PreferenceCategory app:title="@string/advanced">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_download_filled"
app:key="automatic_update_checks"
app:title="@string/show_updates" />
<ListPreference <ListPreference
android:defaultValue="disabled" android:defaultValue="disabled"
android:entries="@array/dataSaverModeOptions" android:entries="@array/dataSaverModeOptions"

View File

@ -77,9 +77,8 @@
<Preference <Preference
android:icon="@drawable/ic_update" android:icon="@drawable/ic_update"
app:key="update" app:key="update"
app:summary="@string/update_summary" app:summary="@string/version"
app:title="App version" /> app:title="@string/update_summary" />
</PreferenceCategory> </PreferenceCategory>