refactor: refactor WelcomeActivity and associated logic (#6996)

This commit is contained in:
Thomas W. 2025-02-05 20:26:45 +01:00 committed by GitHub
parent abc8e49878
commit 3a09869eb6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 170 additions and 107 deletions

View File

@ -6,27 +6,26 @@ import com.github.libretube.api.obj.PipedInstance
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object InstanceHelper {
private const val PIPED_INSTANCES_URL = "https://piped-instances.kavin.rocks"
class InstanceRepository(private val context: Context) {
/**
* Fetch official public instances from kavin.rocks
*/
suspend fun getInstances(context: Context): List<PipedInstance> {
return withContext(Dispatchers.IO) {
runCatching {
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL)
}.getOrNull() ?: run {
throw Exception(context.getString(R.string.failed_fetching_instances))
}
suspend fun getInstances(): Result<List<PipedInstance>> = withContext(Dispatchers.IO) {
runCatching {
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL)
}
}
fun getInstancesFallback(context: Context): List<PipedInstance> {
fun getInstancesFallback(): List<PipedInstance> {
val instanceNames = context.resources.getStringArray(R.array.instances)
return context.resources.getStringArray(R.array.instancesValue)
.mapIndexed { index, instanceValue ->
PipedInstance(instanceNames[index], instanceValue)
}
}
companion object {
private const val PIPED_INSTANCES_URL = "https://piped-instances.kavin.rocks"
}
}

View File

@ -1,9 +1,12 @@
package com.github.libretube.api.obj
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@Parcelize
data class PipedInstance(
val name: String,
@SerialName("api_url") val apiUrl: String,
@ -21,4 +24,4 @@ data class PipedInstance(
@SerialName("uptime_7d") val uptimeWeek: Float? = null,
@SerialName("uptime_30d") val uptimeMonth: Float? = null,
val isCurrentlyDown: Boolean = false
)
) : Parcelable

View File

@ -12,6 +12,8 @@ import com.github.libretube.db.DatabaseHolder.Database
import com.github.libretube.extensions.TAG
import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromStream
@ -42,10 +44,10 @@ object BackupHelper {
* Restore data from a [BackupFile]
*/
@OptIn(ExperimentalSerializationApi::class)
suspend fun restoreAdvancedBackup(context: Context, uri: Uri) {
suspend fun restoreAdvancedBackup(context: Context, uri: Uri) = withContext(Dispatchers.IO) {
val backupFile = context.contentResolver.openInputStream(uri)?.use {
JsonHelper.json.decodeFromStream<BackupFile>(it)
} ?: return
} ?: return@withContext
Database.watchHistoryDao().insertAll(backupFile.watchHistory.orEmpty())
Database.searchHistoryDao().insertAll(backupFile.searchHistory.orEmpty())

View File

@ -7,38 +7,20 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.view.isGone
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityWelcomeBinding
import com.github.libretube.helpers.BackupHelper
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.ui.adapters.InstancesAdapter
import com.github.libretube.ui.base.BaseActivity
import com.github.libretube.ui.models.WelcomeModel
import com.github.libretube.ui.models.WelcomeViewModel
import com.github.libretube.ui.preferences.BackupRestoreSettings
import com.google.common.collect.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class WelcomeActivity : BaseActivity() {
private val viewModel: WelcomeModel by viewModels()
private val viewModel by viewModels<WelcomeViewModel> { WelcomeViewModel.Factory }
private val restoreFilePicker =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) return@registerForActivityResult
CoroutineScope(Dispatchers.IO).launch {
BackupHelper.restoreAdvancedBackup(this@WelcomeActivity, uri)
// only skip the welcome activity if the restored backup contains an instance
val instancePref = PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, "")
if (instancePref.isNotEmpty()) {
withContext(Dispatchers.Main) { startMainActivity() }
}
}
viewModel.restoreAdvancedBackup(this, uri)
}
override fun onCreate(savedInstanceState: Bundle?) {
@ -47,43 +29,38 @@ class WelcomeActivity : BaseActivity() {
val binding = ActivityWelcomeBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.instancesRecycler.layoutManager = LinearLayoutManager(this@WelcomeActivity)
val adapter = InstancesAdapter(viewModel.selectedInstanceIndex.value) { index ->
viewModel.selectedInstanceIndex.value = index
binding.okay.alpha = 1f
}
val adapter = InstancesAdapter(
viewModel.uiState.value?.selectedInstanceIndex,
viewModel::setSelectedInstanceIndex,
)
binding.instancesRecycler.adapter = adapter
// ALl the binding values are optional due to two different possible layouts (normal, landscape)
viewModel.instances.observe(this) { instances ->
adapter.submitList(ImmutableList.copyOf(instances))
binding.progress.isGone = true
}
viewModel.fetchInstances()
binding.okay.alpha = if (viewModel.selectedInstanceIndex.value != null) 1f else 0.5f
binding.okay.setOnClickListener {
if (viewModel.selectedInstanceIndex.value != null) {
val selectedInstance =
viewModel.instances.value!![viewModel.selectedInstanceIndex.value!!]
PreferenceHelper.putString(PreferenceKeys.FETCH_INSTANCE, selectedInstance.apiUrl)
startMainActivity()
} else {
Toast.makeText(this, R.string.choose_instance, Toast.LENGTH_LONG).show()
}
viewModel.saveSelectedInstance()
}
binding.restore.setOnClickListener {
restoreFilePicker.launch(BackupRestoreSettings.JSON)
}
}
private fun startMainActivity() {
// refresh the api urls since they have changed likely
RetrofitInstance.lazyMgr.reset()
val mainActivityIntent = Intent(this@WelcomeActivity, MainActivity::class.java)
startActivity(mainActivityIntent)
finish()
viewModel.uiState.observe(this) { (selectedIndex, instances, error, navigateToMain) ->
binding.okay.isEnabled = selectedIndex != null
binding.progress.isGone = instances.isNotEmpty()
adapter.submitList(instances)
error?.let {
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
viewModel.onErrorShown()
}
navigateToMain?.let {
val mainActivityIntent = Intent(this, MainActivity::class.java)
startActivity(mainActivityIntent)
finish()
viewModel.onNavigated()
}
}
}
override fun requestOrientationChange() {

View File

@ -1,30 +0,0 @@
package com.github.libretube.ui.models
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.github.libretube.api.InstanceHelper
import com.github.libretube.api.obj.PipedInstance
import com.github.libretube.extensions.toastFromMainDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class WelcomeModel(private val application: Application) : AndroidViewModel(application) {
val selectedInstanceIndex = MutableLiveData<Int>()
var instances = MutableLiveData<List<PipedInstance>>()
fun fetchInstances() {
if (!instances.value.isNullOrEmpty()) return
viewModelScope.launch(Dispatchers.IO) {
val instances = try {
InstanceHelper.getInstances(application)
} catch (e: Exception) {
application.toastFromMainDispatcher(e.message.orEmpty())
InstanceHelper.getInstancesFallback(application)
}
this@WelcomeModel.instances.postValue(instances)
}
}
}

View File

@ -0,0 +1,112 @@
package com.github.libretube.ui.models
import android.content.Context
import android.net.Uri
import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.asLiveData
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import com.github.libretube.R
import com.github.libretube.api.InstanceRepository
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PipedInstance
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.helpers.BackupHelper
import com.github.libretube.helpers.PreferenceHelper
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class WelcomeViewModel(
private val instanceRepository: InstanceRepository,
private val savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val _uiState = savedStateHandle.getStateFlow(UI_STATE, UiState())
val uiState = _uiState.asLiveData()
init {
viewModelScope.launch {
instanceRepository.getInstances()
.onSuccess { instances ->
savedStateHandle[UI_STATE] = _uiState.value.copy(instances = instances)
}
.onFailure {
savedStateHandle[UI_STATE] = _uiState.value.copy(
instances = instanceRepository.getInstancesFallback(),
error = R.string.failed_fetching_instances,
)
}
}
}
fun setSelectedInstanceIndex(index: Int) {
savedStateHandle[UI_STATE] = _uiState.value.copy(selectedInstanceIndex = index)
}
fun saveSelectedInstance() {
val selectedInstanceIndex = _uiState.value.selectedInstanceIndex
if (selectedInstanceIndex == null) {
savedStateHandle[UI_STATE] = _uiState.value.copy(error = R.string.choose_instance)
} else {
PreferenceHelper.putString(
PreferenceKeys.FETCH_INSTANCE,
_uiState.value.instances[selectedInstanceIndex].apiUrl
)
refreshAndNavigate()
}
}
fun restoreAdvancedBackup(context: Context, uri: Uri) {
viewModelScope.launch {
BackupHelper.restoreAdvancedBackup(context, uri)
// only skip the welcome activity if the restored backup contains an instance
val instancePref = PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, "")
if (instancePref.isNotEmpty()) {
refreshAndNavigate()
}
}
}
private fun refreshAndNavigate() {
// refresh the api urls since they have changed likely
RetrofitInstance.lazyMgr.reset()
savedStateHandle[UI_STATE] = _uiState.value.copy(navigateToMain = Unit)
}
fun onErrorShown() {
savedStateHandle[UI_STATE] = _uiState.value.copy(error = null)
}
fun onNavigated() {
savedStateHandle[UI_STATE] = _uiState.value.copy(navigateToMain = null)
}
@Parcelize
data class UiState(
val selectedInstanceIndex: Int? = null,
val instances: List<PipedInstance> = emptyList(),
@StringRes val error: Int? = null,
val navigateToMain: Unit? = null,
) : Parcelable
companion object {
private const val UI_STATE = "ui_state"
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
WelcomeViewModel(
instanceRepository = InstanceRepository(this[APPLICATION_KEY]!!),
savedStateHandle = createSavedStateHandle(),
)
}
}
}
}

View File

@ -10,7 +10,7 @@ import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.InstanceHelper
import com.github.libretube.api.InstanceRepository
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PipedInstance
import com.github.libretube.constants.IntentData
@ -53,17 +53,18 @@ class InstanceSettings : BasePreferenceFragment() {
lifecycleScope.launch {
// update the instances to also show custom ones
initInstancesPref(instancePrefs, InstanceHelper.getInstancesFallback(appContext))
initInstancesPref(instancePrefs, InstanceRepository(appContext).getInstancesFallback())
// try to fetch the public list of instances async
try {
val instances = withContext(Dispatchers.IO) {
InstanceHelper.getInstances(appContext)
val instanceRepo = InstanceRepository(appContext)
val instances = instanceRepo.getInstances()
.onFailure {
appContext.toastFromMainDispatcher(it.message.orEmpty())
}
initInstancesPref(instancePrefs, instances)
} catch (e: Exception) {
appContext.toastFromMainDispatcher(e.message.orEmpty())
}
initInstancesPref(
instancePrefs,
instances.getOrDefault(instanceRepo.getInstancesFallback())
)
}
authInstance.setOnPreferenceChangeListener { _, _ ->
@ -189,9 +190,7 @@ class InstanceSettings : BasePreferenceFragment() {
val instances = ImmutableList.copyOf(this.instances)
binding.optionsRecycler.adapter = InstancesAdapter(selectedIndex) {
selectedInstance = instances[it].apiUrl
}.also {
it.submitList(instances)
}
}.also { it.submitList(instances) }
MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)

View File

@ -73,7 +73,9 @@
android:fadeScrollbars="false"
android:paddingBottom="70dp"
android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/instance_row" />
<FrameLayout
android:id="@+id/progress"
@ -109,7 +111,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:alpha="0.5"
android:text="@string/okay" />
</FrameLayout>