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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
object InstanceHelper { class InstanceRepository(private val context: Context) {
private const val PIPED_INSTANCES_URL = "https://piped-instances.kavin.rocks"
/** /**
* Fetch official public instances from kavin.rocks * Fetch official public instances from kavin.rocks
*/ */
suspend fun getInstances(context: Context): List<PipedInstance> { suspend fun getInstances(): Result<List<PipedInstance>> = withContext(Dispatchers.IO) {
return withContext(Dispatchers.IO) {
runCatching { runCatching {
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL) RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL)
}.getOrNull() ?: run {
throw Exception(context.getString(R.string.failed_fetching_instances))
}
} }
} }
fun getInstancesFallback(context: Context): List<PipedInstance> { fun getInstancesFallback(): List<PipedInstance> {
val instanceNames = context.resources.getStringArray(R.array.instances) val instanceNames = context.resources.getStringArray(R.array.instances)
return context.resources.getStringArray(R.array.instancesValue) return context.resources.getStringArray(R.array.instancesValue)
.mapIndexed { index, instanceValue -> .mapIndexed { index, instanceValue ->
PipedInstance(instanceNames[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 package com.github.libretube.api.obj
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@Parcelize
data class PipedInstance( data class PipedInstance(
val name: String, val name: String,
@SerialName("api_url") val apiUrl: String, @SerialName("api_url") val apiUrl: String,
@ -21,4 +24,4 @@ data class PipedInstance(
@SerialName("uptime_7d") val uptimeWeek: Float? = null, @SerialName("uptime_7d") val uptimeWeek: Float? = null,
@SerialName("uptime_30d") val uptimeMonth: Float? = null, @SerialName("uptime_30d") val uptimeMonth: Float? = null,
val isCurrentlyDown: Boolean = false 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.extensions.TAG
import com.github.libretube.obj.BackupFile import com.github.libretube.obj.BackupFile
import com.github.libretube.obj.PreferenceItem import com.github.libretube.obj.PreferenceItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
@ -42,10 +44,10 @@ object BackupHelper {
* Restore data from a [BackupFile] * Restore data from a [BackupFile]
*/ */
@OptIn(ExperimentalSerializationApi::class) @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 { val backupFile = context.contentResolver.openInputStream(uri)?.use {
JsonHelper.json.decodeFromStream<BackupFile>(it) JsonHelper.json.decodeFromStream<BackupFile>(it)
} ?: return } ?: return@withContext
Database.watchHistoryDao().insertAll(backupFile.watchHistory.orEmpty()) Database.watchHistoryDao().insertAll(backupFile.watchHistory.orEmpty())
Database.searchHistoryDao().insertAll(backupFile.searchHistory.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.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.view.isGone 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.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.adapters.InstancesAdapter
import com.github.libretube.ui.base.BaseActivity 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.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() { class WelcomeActivity : BaseActivity() {
private val viewModel: WelcomeModel by viewModels()
private val viewModel by viewModels<WelcomeViewModel> { WelcomeViewModel.Factory }
private val restoreFilePicker = private val restoreFilePicker =
registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
if (uri == null) return@registerForActivityResult if (uri == null) return@registerForActivityResult
CoroutineScope(Dispatchers.IO).launch { viewModel.restoreAdvancedBackup(this, uri)
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() }
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -47,43 +29,38 @@ class WelcomeActivity : BaseActivity() {
val binding = ActivityWelcomeBinding.inflate(layoutInflater) val binding = ActivityWelcomeBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.instancesRecycler.layoutManager = LinearLayoutManager(this@WelcomeActivity) val adapter = InstancesAdapter(
val adapter = InstancesAdapter(viewModel.selectedInstanceIndex.value) { index -> viewModel.uiState.value?.selectedInstanceIndex,
viewModel.selectedInstanceIndex.value = index viewModel::setSelectedInstanceIndex,
binding.okay.alpha = 1f )
}
binding.instancesRecycler.adapter = adapter 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 { binding.okay.setOnClickListener {
if (viewModel.selectedInstanceIndex.value != null) { viewModel.saveSelectedInstance()
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()
}
} }
binding.restore.setOnClickListener { binding.restore.setOnClickListener {
restoreFilePicker.launch(BackupRestoreSettings.JSON) restoreFilePicker.launch(BackupRestoreSettings.JSON)
} }
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()
} }
private fun startMainActivity() { navigateToMain?.let {
// refresh the api urls since they have changed likely val mainActivityIntent = Intent(this, MainActivity::class.java)
RetrofitInstance.lazyMgr.reset()
val mainActivityIntent = Intent(this@WelcomeActivity, MainActivity::class.java)
startActivity(mainActivityIntent) startActivity(mainActivityIntent)
finish() finish()
viewModel.onNavigated()
}
}
} }
override fun requestOrientationChange() { 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.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R 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.RetrofitInstance
import com.github.libretube.api.obj.PipedInstance import com.github.libretube.api.obj.PipedInstance
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
@ -53,17 +53,18 @@ class InstanceSettings : BasePreferenceFragment() {
lifecycleScope.launch { lifecycleScope.launch {
// update the instances to also show custom ones // 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 to fetch the public list of instances async
try { val instanceRepo = InstanceRepository(appContext)
val instances = withContext(Dispatchers.IO) { val instances = instanceRepo.getInstances()
InstanceHelper.getInstances(appContext) .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 { _, _ -> authInstance.setOnPreferenceChangeListener { _, _ ->
@ -189,9 +190,7 @@ class InstanceSettings : BasePreferenceFragment() {
val instances = ImmutableList.copyOf(this.instances) val instances = ImmutableList.copyOf(this.instances)
binding.optionsRecycler.adapter = InstancesAdapter(selectedIndex) { binding.optionsRecycler.adapter = InstancesAdapter(selectedIndex) {
selectedInstance = instances[it].apiUrl selectedInstance = instances[it].apiUrl
}.also { }.also { it.submitList(instances) }
it.submitList(instances)
}
MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title) .setTitle(preference.title)

View File

@ -73,7 +73,9 @@
android:fadeScrollbars="false" android:fadeScrollbars="false"
android:paddingBottom="70dp" android:paddingBottom="70dp"
android:scrollbars="vertical" 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 <FrameLayout
android:id="@+id/progress" android:id="@+id/progress"
@ -109,7 +111,6 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:alpha="0.5"
android:text="@string/okay" /> android:text="@string/okay" />
</FrameLayout> </FrameLayout>