diff --git a/app/src/main/java/com/github/libretube/api/InstanceHelper.kt b/app/src/main/java/com/github/libretube/api/InstanceRepository.kt similarity index 53% rename from app/src/main/java/com/github/libretube/api/InstanceHelper.kt rename to app/src/main/java/com/github/libretube/api/InstanceRepository.kt index 54280da43..59115ce94 100644 --- a/app/src/main/java/com/github/libretube/api/InstanceHelper.kt +++ b/app/src/main/java/com/github/libretube/api/InstanceRepository.kt @@ -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 { - 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> = withContext(Dispatchers.IO) { + runCatching { + RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL) } } - fun getInstancesFallback(context: Context): List { + fun getInstancesFallback(): List { 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" + } } diff --git a/app/src/main/java/com/github/libretube/api/obj/PipedInstance.kt b/app/src/main/java/com/github/libretube/api/obj/PipedInstance.kt index 2d065e5b5..e816bdc5b 100644 --- a/app/src/main/java/com/github/libretube/api/obj/PipedInstance.kt +++ b/app/src/main/java/com/github/libretube/api/obj/PipedInstance.kt @@ -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 diff --git a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt index 311e2e1cc..c0952b776 100644 --- a/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt +++ b/app/src/main/java/com/github/libretube/helpers/BackupHelper.kt @@ -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(it) - } ?: return + } ?: return@withContext Database.watchHistoryDao().insertAll(backupFile.watchHistory.orEmpty()) Database.searchHistoryDao().insertAll(backupFile.searchHistory.orEmpty()) diff --git a/app/src/main/java/com/github/libretube/ui/activities/WelcomeActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/WelcomeActivity.kt index 03e79bb6d..46b003634 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/WelcomeActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/WelcomeActivity.kt @@ -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.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() { diff --git a/app/src/main/java/com/github/libretube/ui/models/WelcomeModel.kt b/app/src/main/java/com/github/libretube/ui/models/WelcomeModel.kt deleted file mode 100644 index 5d4e48883..000000000 --- a/app/src/main/java/com/github/libretube/ui/models/WelcomeModel.kt +++ /dev/null @@ -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() - - var instances = MutableLiveData>() - - 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) - } - } -} diff --git a/app/src/main/java/com/github/libretube/ui/models/WelcomeViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/WelcomeViewModel.kt new file mode 100644 index 000000000..7c31e725c --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/models/WelcomeViewModel.kt @@ -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 = 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(), + ) + } + } + } +} diff --git a/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt index e943bba8a..1d415e201 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt @@ -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) diff --git a/app/src/main/res/layout/activity_welcome.xml b/app/src/main/res/layout/activity_welcome.xml index 117897e1b..48c40bef3 100644 --- a/app/src/main/res/layout/activity_welcome.xml +++ b/app/src/main/res/layout/activity_welcome.xml @@ -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" />