Skip to content

Commit 3a09869

Browse files
refactor: refactor WelcomeActivity and associated logic (libre-tube#6996)
1 parent abc8e49 commit 3a09869

8 files changed

Lines changed: 170 additions & 107 deletions

File tree

app/src/main/java/com/github/libretube/api/InstanceHelper.kt renamed to app/src/main/java/com/github/libretube/api/InstanceRepository.kt

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,26 @@ import com.github.libretube.api.obj.PipedInstance
66
import kotlinx.coroutines.Dispatchers
77
import kotlinx.coroutines.withContext
88

9-
object InstanceHelper {
10-
private const val PIPED_INSTANCES_URL = "https://piped-instances.kavin.rocks"
9+
class InstanceRepository(private val context: Context) {
1110

1211
/**
1312
* Fetch official public instances from kavin.rocks
1413
*/
15-
suspend fun getInstances(context: Context): List<PipedInstance> {
16-
return withContext(Dispatchers.IO) {
17-
runCatching {
18-
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL)
19-
}.getOrNull() ?: run {
20-
throw Exception(context.getString(R.string.failed_fetching_instances))
21-
}
14+
suspend fun getInstances(): Result<List<PipedInstance>> = withContext(Dispatchers.IO) {
15+
runCatching {
16+
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL)
2217
}
2318
}
2419

25-
fun getInstancesFallback(context: Context): List<PipedInstance> {
20+
fun getInstancesFallback(): List<PipedInstance> {
2621
val instanceNames = context.resources.getStringArray(R.array.instances)
2722
return context.resources.getStringArray(R.array.instancesValue)
2823
.mapIndexed { index, instanceValue ->
2924
PipedInstance(instanceNames[index], instanceValue)
3025
}
3126
}
27+
28+
companion object {
29+
private const val PIPED_INSTANCES_URL = "https://piped-instances.kavin.rocks"
30+
}
3231
}

app/src/main/java/com/github/libretube/api/obj/PipedInstance.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.github.libretube.api.obj
22

3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
35
import kotlinx.serialization.SerialName
46
import kotlinx.serialization.Serializable
57

68
@Serializable
9+
@Parcelize
710
data class PipedInstance(
811
val name: String,
912
@SerialName("api_url") val apiUrl: String,
@@ -21,4 +24,4 @@ data class PipedInstance(
2124
@SerialName("uptime_7d") val uptimeWeek: Float? = null,
2225
@SerialName("uptime_30d") val uptimeMonth: Float? = null,
2326
val isCurrentlyDown: Boolean = false
24-
)
27+
) : Parcelable

app/src/main/java/com/github/libretube/helpers/BackupHelper.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import com.github.libretube.db.DatabaseHolder.Database
1212
import com.github.libretube.extensions.TAG
1313
import com.github.libretube.obj.BackupFile
1414
import com.github.libretube.obj.PreferenceItem
15+
import kotlinx.coroutines.Dispatchers
16+
import kotlinx.coroutines.withContext
1517
import kotlinx.serialization.ExperimentalSerializationApi
1618
import kotlinx.serialization.json.booleanOrNull
1719
import kotlinx.serialization.json.decodeFromStream
@@ -42,10 +44,10 @@ object BackupHelper {
4244
* Restore data from a [BackupFile]
4345
*/
4446
@OptIn(ExperimentalSerializationApi::class)
45-
suspend fun restoreAdvancedBackup(context: Context, uri: Uri) {
47+
suspend fun restoreAdvancedBackup(context: Context, uri: Uri) = withContext(Dispatchers.IO) {
4648
val backupFile = context.contentResolver.openInputStream(uri)?.use {
4749
JsonHelper.json.decodeFromStream<BackupFile>(it)
48-
} ?: return
50+
} ?: return@withContext
4951

5052
Database.watchHistoryDao().insertAll(backupFile.watchHistory.orEmpty())
5153
Database.searchHistoryDao().insertAll(backupFile.searchHistory.orEmpty())

app/src/main/java/com/github/libretube/ui/activities/WelcomeActivity.kt

Lines changed: 27 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,20 @@ import android.widget.Toast
77
import androidx.activity.result.contract.ActivityResultContracts
88
import androidx.activity.viewModels
99
import androidx.core.view.isGone
10-
import androidx.recyclerview.widget.LinearLayoutManager
11-
import com.github.libretube.R
12-
import com.github.libretube.api.RetrofitInstance
13-
import com.github.libretube.constants.PreferenceKeys
1410
import com.github.libretube.databinding.ActivityWelcomeBinding
15-
import com.github.libretube.helpers.BackupHelper
16-
import com.github.libretube.helpers.PreferenceHelper
1711
import com.github.libretube.ui.adapters.InstancesAdapter
1812
import com.github.libretube.ui.base.BaseActivity
19-
import com.github.libretube.ui.models.WelcomeModel
13+
import com.github.libretube.ui.models.WelcomeViewModel
2014
import com.github.libretube.ui.preferences.BackupRestoreSettings
21-
import com.google.common.collect.ImmutableList
22-
import kotlinx.coroutines.CoroutineScope
23-
import kotlinx.coroutines.Dispatchers
24-
import kotlinx.coroutines.launch
25-
import kotlinx.coroutines.withContext
2615

2716
class WelcomeActivity : BaseActivity() {
28-
private val viewModel: WelcomeModel by viewModels()
17+
18+
private val viewModel by viewModels<WelcomeViewModel> { WelcomeViewModel.Factory }
2919

3020
private val restoreFilePicker =
3121
registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
3222
if (uri == null) return@registerForActivityResult
33-
CoroutineScope(Dispatchers.IO).launch {
34-
BackupHelper.restoreAdvancedBackup(this@WelcomeActivity, uri)
35-
36-
// only skip the welcome activity if the restored backup contains an instance
37-
val instancePref = PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, "")
38-
if (instancePref.isNotEmpty()) {
39-
withContext(Dispatchers.Main) { startMainActivity() }
40-
}
41-
}
23+
viewModel.restoreAdvancedBackup(this, uri)
4224
}
4325

4426
override fun onCreate(savedInstanceState: Bundle?) {
@@ -47,43 +29,38 @@ class WelcomeActivity : BaseActivity() {
4729
val binding = ActivityWelcomeBinding.inflate(layoutInflater)
4830
setContentView(binding.root)
4931

50-
binding.instancesRecycler.layoutManager = LinearLayoutManager(this@WelcomeActivity)
51-
val adapter = InstancesAdapter(viewModel.selectedInstanceIndex.value) { index ->
52-
viewModel.selectedInstanceIndex.value = index
53-
binding.okay.alpha = 1f
54-
}
32+
val adapter = InstancesAdapter(
33+
viewModel.uiState.value?.selectedInstanceIndex,
34+
viewModel::setSelectedInstanceIndex,
35+
)
5536
binding.instancesRecycler.adapter = adapter
5637

57-
// ALl the binding values are optional due to two different possible layouts (normal, landscape)
58-
viewModel.instances.observe(this) { instances ->
59-
adapter.submitList(ImmutableList.copyOf(instances))
60-
binding.progress.isGone = true
61-
}
62-
viewModel.fetchInstances()
63-
64-
binding.okay.alpha = if (viewModel.selectedInstanceIndex.value != null) 1f else 0.5f
6538
binding.okay.setOnClickListener {
66-
if (viewModel.selectedInstanceIndex.value != null) {
67-
val selectedInstance =
68-
viewModel.instances.value!![viewModel.selectedInstanceIndex.value!!]
69-
PreferenceHelper.putString(PreferenceKeys.FETCH_INSTANCE, selectedInstance.apiUrl)
70-
startMainActivity()
71-
} else {
72-
Toast.makeText(this, R.string.choose_instance, Toast.LENGTH_LONG).show()
73-
}
39+
viewModel.saveSelectedInstance()
7440
}
7541

7642
binding.restore.setOnClickListener {
7743
restoreFilePicker.launch(BackupRestoreSettings.JSON)
7844
}
79-
}
8045

81-
private fun startMainActivity() {
82-
// refresh the api urls since they have changed likely
83-
RetrofitInstance.lazyMgr.reset()
84-
val mainActivityIntent = Intent(this@WelcomeActivity, MainActivity::class.java)
85-
startActivity(mainActivityIntent)
86-
finish()
46+
viewModel.uiState.observe(this) { (selectedIndex, instances, error, navigateToMain) ->
47+
binding.okay.isEnabled = selectedIndex != null
48+
binding.progress.isGone = instances.isNotEmpty()
49+
50+
adapter.submitList(instances)
51+
52+
error?.let {
53+
Toast.makeText(this, it, Toast.LENGTH_LONG).show()
54+
viewModel.onErrorShown()
55+
}
56+
57+
navigateToMain?.let {
58+
val mainActivityIntent = Intent(this, MainActivity::class.java)
59+
startActivity(mainActivityIntent)
60+
finish()
61+
viewModel.onNavigated()
62+
}
63+
}
8764
}
8865

8966
override fun requestOrientationChange() {

app/src/main/java/com/github/libretube/ui/models/WelcomeModel.kt

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package com.github.libretube.ui.models
2+
3+
import android.content.Context
4+
import android.net.Uri
5+
import android.os.Parcelable
6+
import androidx.annotation.StringRes
7+
import androidx.lifecycle.SavedStateHandle
8+
import androidx.lifecycle.ViewModel
9+
import androidx.lifecycle.ViewModelProvider
10+
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
11+
import androidx.lifecycle.asLiveData
12+
import androidx.lifecycle.createSavedStateHandle
13+
import androidx.lifecycle.viewModelScope
14+
import androidx.lifecycle.viewmodel.initializer
15+
import androidx.lifecycle.viewmodel.viewModelFactory
16+
import com.github.libretube.R
17+
import com.github.libretube.api.InstanceRepository
18+
import com.github.libretube.api.RetrofitInstance
19+
import com.github.libretube.api.obj.PipedInstance
20+
import com.github.libretube.constants.PreferenceKeys
21+
import com.github.libretube.helpers.BackupHelper
22+
import com.github.libretube.helpers.PreferenceHelper
23+
import kotlinx.coroutines.launch
24+
import kotlinx.parcelize.Parcelize
25+
26+
class WelcomeViewModel(
27+
private val instanceRepository: InstanceRepository,
28+
private val savedStateHandle: SavedStateHandle,
29+
) : ViewModel() {
30+
31+
private val _uiState = savedStateHandle.getStateFlow(UI_STATE, UiState())
32+
val uiState = _uiState.asLiveData()
33+
34+
init {
35+
viewModelScope.launch {
36+
instanceRepository.getInstances()
37+
.onSuccess { instances ->
38+
savedStateHandle[UI_STATE] = _uiState.value.copy(instances = instances)
39+
}
40+
.onFailure {
41+
savedStateHandle[UI_STATE] = _uiState.value.copy(
42+
instances = instanceRepository.getInstancesFallback(),
43+
error = R.string.failed_fetching_instances,
44+
)
45+
}
46+
}
47+
}
48+
49+
fun setSelectedInstanceIndex(index: Int) {
50+
savedStateHandle[UI_STATE] = _uiState.value.copy(selectedInstanceIndex = index)
51+
}
52+
53+
fun saveSelectedInstance() {
54+
val selectedInstanceIndex = _uiState.value.selectedInstanceIndex
55+
if (selectedInstanceIndex == null) {
56+
savedStateHandle[UI_STATE] = _uiState.value.copy(error = R.string.choose_instance)
57+
} else {
58+
PreferenceHelper.putString(
59+
PreferenceKeys.FETCH_INSTANCE,
60+
_uiState.value.instances[selectedInstanceIndex].apiUrl
61+
)
62+
refreshAndNavigate()
63+
}
64+
}
65+
66+
fun restoreAdvancedBackup(context: Context, uri: Uri) {
67+
viewModelScope.launch {
68+
BackupHelper.restoreAdvancedBackup(context, uri)
69+
70+
// only skip the welcome activity if the restored backup contains an instance
71+
val instancePref = PreferenceHelper.getString(PreferenceKeys.FETCH_INSTANCE, "")
72+
if (instancePref.isNotEmpty()) {
73+
refreshAndNavigate()
74+
}
75+
}
76+
}
77+
78+
private fun refreshAndNavigate() {
79+
// refresh the api urls since they have changed likely
80+
RetrofitInstance.lazyMgr.reset()
81+
savedStateHandle[UI_STATE] = _uiState.value.copy(navigateToMain = Unit)
82+
}
83+
84+
fun onErrorShown() {
85+
savedStateHandle[UI_STATE] = _uiState.value.copy(error = null)
86+
}
87+
88+
fun onNavigated() {
89+
savedStateHandle[UI_STATE] = _uiState.value.copy(navigateToMain = null)
90+
}
91+
92+
@Parcelize
93+
data class UiState(
94+
val selectedInstanceIndex: Int? = null,
95+
val instances: List<PipedInstance> = emptyList(),
96+
@StringRes val error: Int? = null,
97+
val navigateToMain: Unit? = null,
98+
) : Parcelable
99+
100+
companion object {
101+
private const val UI_STATE = "ui_state"
102+
103+
val Factory: ViewModelProvider.Factory = viewModelFactory {
104+
initializer {
105+
WelcomeViewModel(
106+
instanceRepository = InstanceRepository(this[APPLICATION_KEY]!!),
107+
savedStateHandle = createSavedStateHandle(),
108+
)
109+
}
110+
}
111+
}
112+
}

app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import androidx.preference.Preference
1010
import androidx.preference.SwitchPreferenceCompat
1111
import androidx.recyclerview.widget.LinearLayoutManager
1212
import com.github.libretube.R
13-
import com.github.libretube.api.InstanceHelper
13+
import com.github.libretube.api.InstanceRepository
1414
import com.github.libretube.api.RetrofitInstance
1515
import com.github.libretube.api.obj.PipedInstance
1616
import com.github.libretube.constants.IntentData
@@ -53,17 +53,18 @@ class InstanceSettings : BasePreferenceFragment() {
5353

5454
lifecycleScope.launch {
5555
// update the instances to also show custom ones
56-
initInstancesPref(instancePrefs, InstanceHelper.getInstancesFallback(appContext))
56+
initInstancesPref(instancePrefs, InstanceRepository(appContext).getInstancesFallback())
5757

5858
// try to fetch the public list of instances async
59-
try {
60-
val instances = withContext(Dispatchers.IO) {
61-
InstanceHelper.getInstances(appContext)
59+
val instanceRepo = InstanceRepository(appContext)
60+
val instances = instanceRepo.getInstances()
61+
.onFailure {
62+
appContext.toastFromMainDispatcher(it.message.orEmpty())
6263
}
63-
initInstancesPref(instancePrefs, instances)
64-
} catch (e: Exception) {
65-
appContext.toastFromMainDispatcher(e.message.orEmpty())
66-
}
64+
initInstancesPref(
65+
instancePrefs,
66+
instances.getOrDefault(instanceRepo.getInstancesFallback())
67+
)
6768
}
6869

6970
authInstance.setOnPreferenceChangeListener { _, _ ->
@@ -189,9 +190,7 @@ class InstanceSettings : BasePreferenceFragment() {
189190
val instances = ImmutableList.copyOf(this.instances)
190191
binding.optionsRecycler.adapter = InstancesAdapter(selectedIndex) {
191192
selectedInstance = instances[it].apiUrl
192-
}.also {
193-
it.submitList(instances)
194-
}
193+
}.also { it.submitList(instances) }
195194

196195
MaterialAlertDialogBuilder(requireContext())
197196
.setTitle(preference.title)

app/src/main/res/layout/activity_welcome.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@
7373
android:fadeScrollbars="false"
7474
android:paddingBottom="70dp"
7575
android:scrollbars="vertical"
76-
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
76+
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
77+
app:layout_behavior="@string/appbar_scrolling_view_behavior"
78+
tools:listitem="@layout/instance_row" />
7779

7880
<FrameLayout
7981
android:id="@+id/progress"
@@ -109,7 +111,6 @@
109111
android:layout_width="wrap_content"
110112
android:layout_height="wrap_content"
111113
android:layout_gravity="end"
112-
android:alpha="0.5"
113114
android:text="@string/okay" />
114115

115116
</FrameLayout>

0 commit comments

Comments
 (0)