mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'recloudstream:master' into master
This commit is contained in:
commit
05dd490ab7
14 changed files with 188 additions and 45 deletions
app
build.gradle.kts
src/main
java/com/lagradost/cloudstream3
res/layout
|
@ -58,8 +58,8 @@ android {
|
|||
minSdk = 21
|
||||
targetSdk = 33
|
||||
|
||||
versionCode = 60
|
||||
versionName = "4.1.9"
|
||||
versionCode = 62
|
||||
versionName = "4.2.0"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
|
||||
|
|
|
@ -497,6 +497,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
R.id.navigation_results_phone,
|
||||
R.id.navigation_results_tv,
|
||||
R.id.navigation_player,
|
||||
R.id.navigation_quick_search,
|
||||
).contains(destination.id)
|
||||
|
||||
binding?.navHostFragment?.apply {
|
||||
|
@ -1101,15 +1102,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
updateTv()
|
||||
|
||||
// backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
|
||||
try {
|
||||
normalSafeApiCall {
|
||||
val appVer = BuildConfig.VERSION_NAME
|
||||
val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: ""
|
||||
if (appVer != lastAppAutoBackup) {
|
||||
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
|
||||
backup()
|
||||
normalSafeApiCall {
|
||||
backup()
|
||||
}
|
||||
normalSafeApiCall {
|
||||
// Recompile oat on new version
|
||||
PluginManager.deleteAllOatFiles(this)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
|
||||
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
|
||||
|
|
|
@ -137,6 +137,20 @@ object PluginManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all generated oat files which will force Android to recompile the dex extensions.
|
||||
* This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update.
|
||||
*/
|
||||
fun deleteAllOatFiles(context: Context) {
|
||||
File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo ->
|
||||
repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file ->
|
||||
val success = file.deleteRecursively()
|
||||
Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getPluginsOnline(): Array<PluginData> {
|
||||
return getKey(PLUGINS_KEY) ?: emptyArray()
|
||||
}
|
||||
|
|
|
@ -461,6 +461,11 @@ class HomeParentItemAdapterPreview(
|
|||
}
|
||||
}
|
||||
|
||||
homePreviewSearchButton.setOnClickListener { _ ->
|
||||
// Open blank screen.
|
||||
viewModel.queryTextSubmit("")
|
||||
}
|
||||
|
||||
// This makes the hidden next buttons only available when on the info button
|
||||
// Otherwise you might be able to go to the next item without being at the info button
|
||||
homePreviewInfoBtt.setOnFocusChangeListener { _, hasFocus ->
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.safeApiCall
|
||||
import com.lagradost.cloudstream3.ui.result.ResultEpisode
|
||||
|
@ -15,6 +16,7 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip
|
|||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlayerGeneratorViewModel : ViewModel() {
|
||||
companion object {
|
||||
|
@ -38,6 +40,11 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
private val _currentSubtitleYear = MutableLiveData<Int?>(null)
|
||||
val currentSubtitleYear: LiveData<Int?> = _currentSubtitleYear
|
||||
|
||||
/**
|
||||
* Save the Episode ID to prevent starting multiple link loading Jobs when preloading links.
|
||||
*/
|
||||
private var currentLoadingEpisodeId: Int? = null
|
||||
|
||||
fun setSubtitleYear(year: Int?) {
|
||||
_currentSubtitleYear.postValue(year)
|
||||
}
|
||||
|
@ -72,18 +79,32 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun preLoadNextLinks() {
|
||||
val id = getId()
|
||||
// Do not preload if already loading
|
||||
if (id == currentLoadingEpisodeId) return
|
||||
|
||||
Log.i(TAG, "preLoadNextLinks")
|
||||
currentJob?.cancel()
|
||||
currentJob = viewModelScope.launchSafe {
|
||||
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
||||
safeApiCall {
|
||||
generator?.generateLinks(
|
||||
type = LoadType.InApp,
|
||||
clearCache = false,
|
||||
callback = {},
|
||||
subtitleCallback = {},
|
||||
offset = 1
|
||||
)
|
||||
currentLoadingEpisodeId = id
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
try {
|
||||
if (generator?.hasCache == true && generator?.hasNext() == true) {
|
||||
safeApiCall {
|
||||
generator?.generateLinks(
|
||||
type = LoadType.InApp,
|
||||
clearCache = false,
|
||||
callback = {},
|
||||
subtitleCallback = {},
|
||||
offset = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
} finally {
|
||||
if (currentLoadingEpisodeId == id) {
|
||||
currentLoadingEpisodeId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -162,14 +183,14 @@ class PlayerGeneratorViewModel : ViewModel() {
|
|||
// load more data
|
||||
_loadingLinks.postValue(Resource.Loading())
|
||||
val loadingState = safeApiCall {
|
||||
generator?.generateLinks(type = type,clearCache = clearCache, callback = {
|
||||
generator?.generateLinks(type = type, clearCache = clearCache, callback = {
|
||||
currentLinks.add(it)
|
||||
// Clone to prevent ConcurrentModificationException
|
||||
normalSafeApiCall {
|
||||
// Extra normalSafeApiCall since .toSet() iterates.
|
||||
_currentLinks.postValue(currentLinks.toSet())
|
||||
}
|
||||
}, subtitleCallback = {
|
||||
}, subtitleCallback = {
|
||||
currentSubs.add(it)
|
||||
normalSafeApiCall {
|
||||
_currentSubs.postValue(currentSubs.toSet())
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
|||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.search.SearchViewModel
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.ownShow
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
|
@ -269,6 +270,10 @@ class QuickSearchFragment : Fragment() {
|
|||
activity?.popCurrentPage()
|
||||
}
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
binding?.quickSearch?.requestFocus()
|
||||
}
|
||||
|
||||
arguments?.getString(AUTOSEARCH_KEY)?.let {
|
||||
binding?.quickSearch?.setQuery(it, true)
|
||||
arguments?.remove(AUTOSEARCH_KEY)
|
||||
|
|
|
@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.result
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -12,7 +14,10 @@ import com.lagradost.cloudstream3.R
|
|||
import com.lagradost.cloudstream3.databinding.CastItemBinding
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class ActorAdaptor(
|
||||
private var nextFocusUpId: Int? = null,
|
||||
private val focusCallback: (View?) -> Unit = {}
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
data class ActorMetaData(
|
||||
var isInverted: Boolean,
|
||||
val actor: ActorData,
|
||||
|
@ -22,7 +27,8 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return CardViewHolder(
|
||||
CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback
|
||||
CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
focusCallback
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -64,10 +70,10 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV
|
|||
}
|
||||
}
|
||||
|
||||
private class CardViewHolder
|
||||
private inner class CardViewHolder
|
||||
constructor(
|
||||
val binding: CastItemBinding,
|
||||
private val focusCallback : (View?) -> Unit = {}
|
||||
private val focusCallback: (View?) -> Unit = {}
|
||||
) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
|
@ -78,8 +84,18 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV
|
|||
Pair(actor.voiceActor?.image, actor.actor.image)
|
||||
}
|
||||
|
||||
// Fix tv focus escaping the recyclerview
|
||||
if (position == 0) {
|
||||
itemView.nextFocusLeftId = R.id.result_cast_items
|
||||
} else if ((position - 1) == itemCount) {
|
||||
itemView.nextFocusRightId = R.id.result_cast_items
|
||||
}
|
||||
nextFocusUpId?.let {
|
||||
itemView.nextFocusUpId = it
|
||||
}
|
||||
|
||||
itemView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if(hasFocus) {
|
||||
if (hasFocus) {
|
||||
focusCallback(v)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,10 +114,20 @@ class ResultFragmentTv : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun hasNoFocus(): Boolean {
|
||||
val focus = activity?.currentFocus
|
||||
if (focus == null || !focus.isVisible) return true
|
||||
return focus == binding?.resultRoot
|
||||
// private fun hasNoFocus(): Boolean {
|
||||
// val focus = activity?.currentFocus
|
||||
// if (focus == null || !focus.isVisible) return true
|
||||
// return focus == binding?.resultRoot
|
||||
// }
|
||||
|
||||
/**
|
||||
* Force focus any play button.
|
||||
* Note that this will steal any focus if the episode loading is too slow (unlikely).
|
||||
*/
|
||||
private fun focusPlayButton() {
|
||||
binding?.resultPlayMovie?.requestFocus()
|
||||
binding?.resultPlaySeries?.requestFocus()
|
||||
binding?.resultResumeSeries?.requestFocus()
|
||||
}
|
||||
|
||||
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
||||
|
@ -413,7 +423,13 @@ class ResultFragmentTv : Fragment() {
|
|||
setHorizontal()
|
||||
}
|
||||
|
||||
resultCastItems.adapter = ActorAdaptor {
|
||||
val aboveCast = listOf(
|
||||
binding?.resultEpisodesShow,
|
||||
binding?.resultBookmarkButton,
|
||||
).firstOrNull {
|
||||
it?.isVisible == true
|
||||
}
|
||||
resultCastItems.adapter = ActorAdaptor(aboveCast?.id) {
|
||||
toggleEpisodes(false)
|
||||
}
|
||||
}
|
||||
|
@ -454,9 +470,7 @@ class ResultFragmentTv : Fragment() {
|
|||
resultPlaySeries.isVisible = false
|
||||
resultResumeSeries.isVisible = true
|
||||
|
||||
if (hasNoFocus()) {
|
||||
resultResumeSeries.requestFocus()
|
||||
}
|
||||
focusPlayButton()
|
||||
|
||||
resultResumeSeries.text =
|
||||
if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull(
|
||||
|
@ -539,9 +553,7 @@ class ResultFragmentTv : Fragment() {
|
|||
)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
if (hasNoFocus()) {
|
||||
resultPlayMovie.requestFocus()
|
||||
}
|
||||
focusPlayButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -636,6 +648,9 @@ class ResultFragmentTv : Fragment() {
|
|||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Used to request focus the first time the episodes are loaded.
|
||||
var hasLoadedEpisodesOnce = false
|
||||
observeNullable(viewModel.episodes) { episodes ->
|
||||
binding?.apply {
|
||||
resultEpisodes.isVisible = episodes is Resource.Success
|
||||
|
@ -663,6 +678,10 @@ class ResultFragmentTv : Fragment() {
|
|||
)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
if (!hasLoadedEpisodesOnce) {
|
||||
hasLoadedEpisodesOnce = true
|
||||
focusPlayButton()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -205,6 +205,11 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default focus on TV
|
||||
if (isTrueTv) {
|
||||
settingsGeneral.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -109,18 +109,19 @@ class InAppUpdater {
|
|||
releases.sortedWith(compareBy {
|
||||
versionRegex.find(it.name)?.groupValues?.get(2)
|
||||
}).toList().lastOrNull()*/
|
||||
val found =
|
||||
val foundList =
|
||||
response.filter { rel ->
|
||||
!rel.prerelease
|
||||
}.sortedWith(compareBy { release ->
|
||||
release.assets.filter { it.content_type == "application/vnd.android.package-archive" }
|
||||
.getOrNull(0)?.name?.let { it1 ->
|
||||
release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 ->
|
||||
versionRegex.find(
|
||||
it1
|
||||
)?.groupValues?.get(2)
|
||||
)?.groupValues?.let {
|
||||
it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt()
|
||||
}
|
||||
}
|
||||
}).toList().lastOrNull()
|
||||
|
||||
}).toList()
|
||||
val found = foundList.lastOrNull()
|
||||
val foundAsset = found?.assets?.getOrNull(0)
|
||||
val currentVersion = packageName?.let {
|
||||
packageManager.getPackageInfo(
|
||||
|
|
|
@ -114,6 +114,22 @@
|
|||
android:nextFocusRight="@id/home_switch_account"
|
||||
/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_preview_search_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:contentDescription="@string/search"
|
||||
android:nextFocusLeft="@id/home_change_api"
|
||||
android:nextFocusRight="@id/home_preview_switch_account"
|
||||
android:nextFocusDown="@id/home_preview_info_btt"
|
||||
android:padding="10dp"
|
||||
android:visibility="gone"
|
||||
android:src="@drawable/search_icon"
|
||||
android:tag="@string/tv_no_focus_tag"
|
||||
app:tint="@color/player_on_button_tv_attr" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_switch_account"
|
||||
|
||||
|
|
|
@ -45,9 +45,24 @@
|
|||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:gravity="center_vertical"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/home_preview_switch_account"
|
||||
android:nextFocusRight="@id/home_preview_search_button"
|
||||
android:nextFocusDown="@id/home_preview_play_btt" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_preview_search_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:contentDescription="@string/search"
|
||||
android:nextFocusLeft="@id/home_preview_change_api"
|
||||
android:nextFocusRight="@id/home_preview_switch_account"
|
||||
android:nextFocusDown="@id/home_preview_info_btt"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/search_icon"
|
||||
android:tag="@string/tv_no_focus_tag"
|
||||
app:tint="@color/player_on_button_tv_attr" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_preview_switch_account"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -55,7 +70,8 @@
|
|||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:contentDescription="@string/account"
|
||||
android:nextFocusLeft="@id/home_preview_change_api"
|
||||
android:nextFocusLeft="@id/home_preview_search_button"
|
||||
android:nextFocusRight="@id/home_preview_switch_account"
|
||||
android:nextFocusDown="@id/home_preview_info_btt"
|
||||
|
||||
android:padding="10dp"
|
||||
|
|
|
@ -113,7 +113,25 @@
|
|||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:gravity="center_vertical"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusRight="@id/home_switch_account" />
|
||||
android:nextFocusRight="@id/home_preview_search_button"
|
||||
android:nextFocusDown="@id/home_preview_play_btt" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_preview_search_button"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/search"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@id/home_change_api"
|
||||
android:nextFocusRight="@id/home_switch_account"
|
||||
android:nextFocusDown="@id/home_preview_info_btt"
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/search_icon"
|
||||
android:tag="@string/tv_no_focus_tag"
|
||||
app:tint="@color/player_on_button_tv_attr" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/home_switch_account"
|
||||
|
@ -122,7 +140,9 @@
|
|||
android:layout_gravity="end"
|
||||
android:background="@drawable/player_button_tv_attr_no_bg"
|
||||
android:contentDescription="@string/account"
|
||||
android:nextFocusLeft="@id/home_change_api"
|
||||
android:nextFocusLeft="@id/home_preview_search_button"
|
||||
android:nextFocusRight="@id/home_switch_account"
|
||||
android:nextFocusDown="@id/home_change_api"
|
||||
|
||||
android:padding="10dp"
|
||||
android:src="@drawable/ic_outline_account_circle_24"
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
android:id="@+id/search_autofit_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
android:clipToPadding="false"
|
||||
android:descendantFocusability="afterDescendants"
|
||||
|
|
Loading…
Reference in a new issue