Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: refactor WelcomeActivity and associated logic #6996

Merged
merged 7 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading