Skip to content

Commit

Permalink
refactor: refactor WelcomeActivity and associated logic (#6996)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pittvandewitt authored Feb 5, 2025
1 parent abc8e49 commit 3a09869
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?) {
Expand All @@ -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() {
Expand Down
30 changes: 0 additions & 30 deletions app/src/main/java/com/github/libretube/ui/models/WelcomeModel.kt

This file was deleted.

112 changes: 112 additions & 0 deletions app/src/main/java/com/github/libretube/ui/models/WelcomeViewModel.kt
Original file line number Diff line number Diff line change
@@ -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(),
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 { _, _ ->
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/res/layout/activity_welcome.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit 3a09869

Please sign in to comment.