Merge branch 'master' into master

This commit is contained in:
Osten 2023-08-01 00:44:01 +02:00 committed by GitHub
commit a0c112ad3c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
201 changed files with 9890 additions and 6807 deletions

View file

@ -32,10 +32,10 @@ jobs:
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive"
- uses: actions/checkout@v2
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '11'
java-version: '17'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew

View file

@ -42,10 +42,10 @@ jobs:
cd $GITHUB_WORKSPACE/dokka/
rm -rf "./-cloudstream"
- name: Setup JDK 11
- name: Setup JDK 17
uses: actions/setup-java@v1
with:
java-version: 11
java-version: 17
- name: Setup Android SDK
uses: android-actions/setup-android@v2

View file

@ -24,10 +24,10 @@ jobs:
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
- uses: actions/checkout@v2
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '11'
java-version: '17'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew

View file

@ -7,10 +7,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 11
- name: Set up JDK 17
uses: actions/setup-java@v2
with:
java-version: '11'
java-version: '17'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
<bytecodeTargetLevel target="17" />
</component>
</project>

View file

@ -8,7 +8,6 @@
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="11" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />

View file

@ -7,7 +7,6 @@ plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-android-extensions")
id("org.jetbrains.dokka")
}
@ -28,6 +27,11 @@ android {
testOptions {
unitTests.isReturnDefaultValues = true
}
viewBinding {
enable = true
}
signingConfigs {
create("prerelease") {
if (prereleaseStoreFile != null) {
@ -48,7 +52,7 @@ android {
targetSdk = 33
versionCode = 59
versionName = "4.0.1"
versionName = "4.1.1"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
@ -143,6 +147,7 @@ dependencies {
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
androidTestImplementation("androidx.test:core")
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
// implementation("org.jsoup:jsoup:1.13.1")
@ -215,7 +220,7 @@ dependencies {
implementation("com.github.discord:OverlappingPanels:0.1.3")
// debugImplementation because LeakCanary should only run in debug builds.
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
//debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
// for shimmer when loading
implementation("com.facebook.shimmer:shimmer:0.5.0")
@ -229,13 +234,13 @@ dependencies {
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204
implementation("com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT")
implementation("com.github.TeamNewPipe:NewPipeExtractor:8495ad619e")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance
implementation("me.xdrop:fuzzywuzzy:1.4.0")
// color pallette for images -> colors
// color palette for images -> colors
implementation("androidx.palette:palette-ktx:1.0.0")
}

View file

@ -1,6 +1,30 @@
package com.lagradost.cloudstream3
import android.app.Activity
import android.os.Bundle
import android.os.PersistableBundle
import android.view.LayoutInflater
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding
import com.lagradost.cloudstream3.databinding.FragmentResultBinding
import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.FragmentSearchTvBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.databinding.HomepageParentTvBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutTvBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
import com.lagradost.cloudstream3.databinding.TrailerCustomLayoutBinding
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.runBlocking
@ -8,16 +32,23 @@ import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class TestApplication : Activity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
}
}
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
private fun getAllProviders(): List<MainAPI> {
private fun getAllProviders(): Array<MainAPI> {
println("Providers: ${APIHolder.allProviders.size}")
return APIHolder.allProviders //.filter { !it.usesWebView }
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
}
@Test
@ -26,6 +57,73 @@ class ExampleInstrumentedTest {
println("Done providersExist")
}
@Throws
private inline fun <reified T : ViewBinding> testAllLayouts(
activity: Activity,
vararg layouts: Int
) {
val bind = T::class.java.methods.first { it.name == "bind" }
val inflater = LayoutInflater.from(activity)
for (layout in layouts) {
val root = inflater.inflate(layout, null, false)
bind.invoke(null, root)
}
}
@Test
@Throws
fun layoutTest() {
ActivityScenario.launch(MainActivity::class.java).use { scenario ->
scenario.onActivity { activity: MainActivity ->
// FragmentHomeHeadBinding and FragmentHomeHeadTvBinding CANT be the same
//testAllLayouts<FragmentHomeHeadBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
//testAllLayouts<FragmentHomeHeadTvBinding>(activity, R.layout.fragment_home_head, R.layout.fragment_home_head_tv)
// main cant be tested
// testAllLayouts<ActivityMainTvBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
// testAllLayouts<ActivityMainBinding>(activity,R.layout.activity_main, R.layout.activity_main_tv)
//testAllLayouts<ActivityMainBinding>(activity, R.layout.activity_main_tv)
testAllLayouts<FragmentPlayerBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
testAllLayouts<FragmentPlayerTvBinding>(activity, R.layout.fragment_player,R.layout.fragment_player_tv)
// testAllLayouts<FragmentResultBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
// testAllLayouts<FragmentResultTvBinding>(activity, R.layout.fragment_result,R.layout.fragment_result_tv)
testAllLayouts<PlayerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
testAllLayouts<PlayerCustomLayoutTvBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
testAllLayouts<TrailerCustomLayoutBinding>(activity, R.layout.player_custom_layout,R.layout.player_custom_layout_tv, R.layout.trailer_custom_layout)
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<RepositoryItemBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<RepositoryItemTvBinding>(activity, R.layout.repository_item_tv, R.layout.repository_item)
testAllLayouts<FragmentHomeBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
testAllLayouts<FragmentHomeTvBinding>(activity, R.layout.fragment_home_tv, R.layout.fragment_home)
testAllLayouts<FragmentSearchBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
testAllLayouts<FragmentSearchTvBinding>(activity, R.layout.fragment_search_tv, R.layout.fragment_search)
testAllLayouts<HomeResultGridBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid)
//testAllLayouts<HomeResultGridExpandedBinding>(activity, R.layout.home_result_grid_expanded, R.layout.home_result_grid) ??? fails ???
testAllLayouts<SearchResultGridExpandedBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
testAllLayouts<SearchResultGridBinding>(activity, R.layout.search_result_grid, R.layout.search_result_grid_expanded)
// testAllLayouts<HomeScrollViewBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
// testAllLayouts<HomeScrollViewTvBinding>(activity, R.layout.home_scroll_view, R.layout.home_scroll_view_tv)
testAllLayouts<HomepageParentTvBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
testAllLayouts<HomepageParentBinding>(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent)
}
}
}
@Test
@Throws(AssertionError::class)
fun providerCorrectData() {
@ -49,7 +147,7 @@ class ExampleInstrumentedTest {
@Test
fun providerCorrectHomepage() {
runBlocking {
getAllProviders().amap { api ->
getAllProviders().toList().amap { api ->
TestingUtils.testHomepage(api, ::println)
}
}

View file

@ -51,8 +51,8 @@ class CustomReportSender : ReportSender {
thread { // to not run it on main thread
runBlocking {
suspendSafeApiCall {
val post = app.post(url, data = data)
println("Report response: $post")
app.post(url, data = data)
//println("Report response: $post")
}
}
}

View file

@ -9,6 +9,7 @@ import android.content.res.Resources
import android.os.Build
import android.util.Log
import android.view.*
import android.view.View.NO_ID
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
@ -27,6 +28,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper
@ -34,9 +36,18 @@ import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.schabi.newpipe.extractor.NewPipe
import java.lang.ref.WeakReference
import java.util.*
object CommonActivity {
private var _activity: WeakReference<Activity>? = null
var activity
get() = _activity?.get()
private set(value) {
_activity = WeakReference(value)
}
@MainThread
fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
@ -56,6 +67,30 @@ object CommonActivity {
var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return
act.runOnUiThread {
showToast(act, act.getString(message), duration)
}
}
fun showToast(message: String?, duration: Int? = null) {
val act = activity ?: return
act.runOnUiThread {
showToast(act, message, duration)
}
}
fun showToast(message: UiText?, duration: Int? = null) {
val act = activity ?: return
if (message == null) return
act.runOnUiThread {
showToast(act, message.asString(act), duration)
}
}
@MainThread
fun showToast(act: Activity?, text: UiText, duration: Int) {
if (act == null) return
text.asStringNull(act)?.let {
@ -140,6 +175,7 @@ object CommonActivity {
fun init(act: ComponentActivity?) {
if (act == null) return
activity = act
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode =
@ -260,27 +296,58 @@ object CommonActivity {
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
}
/** because we want closes find, aka when multiple have the same id, we go to parent
until the correct one is found */
private fun localLook(from: View, id: Int): View? {
if (id == NO_ID) return null
var currentLook: View = from
while (true) {
currentLook.findViewById<View?>(id)?.let { return it }
currentLook = (currentLook.parent as? View) ?: break
}
return null
}
/*var currentLook: View = view
while (true) {
val tmpNext = currentLook.findViewById<View?>(nextId)
if (tmpNext != null) {
next = tmpNext
break
}
currentLook = currentLook.parent as? View ?: break
}*/
/** recursively looks for a next focus up to a depth of 10,
* this is used to override the normal shit focus system
* because this application has a lot of invisible views that messes with some tv devices*/
private fun getNextFocus(
act: Activity?,
view: View?,
direction: FocusDirection,
depth: Int = 0
): Int? {
): View? {
// if input is invalid let android decide + depth test to not crash if loop is found
if (view == null || depth >= 10 || act == null) {
return null
}
val nextId = when (direction) {
FocusDirection.Left -> {
view.nextFocusLeftId
var nextId = when (direction) {
FocusDirection.Start -> {
if (view.isRtl())
view.nextFocusRightId
else
view.nextFocusLeftId
}
FocusDirection.Up -> {
view.nextFocusUpId
}
FocusDirection.Right -> {
view.nextFocusRightId
FocusDirection.End -> {
if (view.isRtl())
view.nextFocusLeftId
else
view.nextFocusRightId
}
FocusDirection.Down -> {
@ -288,27 +355,35 @@ object CommonActivity {
}
}
return if (nextId != -1) {
val next = act.findViewById<View?>(nextId)
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
if (next?.isShown == false) {
getNextFocus(act, next, direction, depth + 1)
} else {
if (depth == 0) {
null
} else {
nextId
}
}
} else {
null
if (nextId == NO_ID) {
// if not specified then use forward id
nextId = view.nextFocusForwardId
// if view is still not found to next focus then return and let android decide
if (nextId == NO_ID) return null
}
var next = act.findViewById<View?>(nextId) ?: return null
next = localLook(view, nextId) ?: next
var currentLook: View = view
while (currentLook.findViewById<View?>(nextId)?.also { next = it } == null) {
currentLook = (currentLook.parent as? View) ?: break
}
// if cant focus but visible then break and let android decide
if (!next.isFocusable && next.isShown) return null
// if not shown then continue because we will "skip" over views to get to a replacement
if (!next.isShown) return getNextFocus(act, next, direction, depth + 1)
// nothing wrong with the view found, return it
return next
}
enum class FocusDirection {
Left,
Right,
private enum class FocusDirection {
Start,
End,
Up,
Down,
}
@ -407,67 +482,64 @@ object CommonActivity {
//}
}
/** overrides focus and custom key events */
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
if (act == null) return null
val currentFocus = act.currentFocus
event?.keyCode?.let { keyCode ->
when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (act.currentFocus != null) {
val next = when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Left
)
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
val nextView = when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
act,
currentFocus,
FocusDirection.Start
)
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Right
)
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
act,
currentFocus,
FocusDirection.End
)
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Up
)
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
act,
currentFocus,
FocusDirection.Up
)
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Down
)
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
act,
currentFocus,
FocusDirection.Down
)
else -> null
}
if (next != null && next != -1) {
val nextView = act.findViewById<View?>(next)
if (nextView != null) {
nextView.requestFocus()
keyEventListener?.invoke(Pair(event, true))
return true
}
}
when (keyCode) {
KeyEvent.KEYCODE_DPAD_CENTER -> {
if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
UIHelper.showInputMethod(act.currentFocus?.findFocus())
}
}
}
}
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
}
else -> null
}
if (nextView != null) {
nextView.requestFocus()
keyEventListener?.invoke(Pair(event, true))
return true
}
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) {
UIHelper.showInputMethod(act.currentFocus?.findFocus())
}
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
}
// if someone else want to override the focus then don't handle the event as it is already
// consumed. used in video player
if (keyEventListener?.invoke(Pair(event, false)) == true) {
return true
}

View file

@ -50,8 +50,10 @@ object APIHolder {
val allProviders = threadSafeListOf<MainAPI>()
fun initAll() {
for (api in allProviders) {
api.init()
synchronized(allProviders) {
for (api in allProviders) {
api.init()
}
}
apiMap = null
}
@ -64,27 +66,35 @@ object APIHolder {
var apiMap: Map<String, Int>? = null
fun addPluginMapping(plugin: MainAPI) {
apis = apis + plugin
synchronized(apis) {
apis = apis + plugin
}
initMap(true)
}
fun removePluginMapping(plugin: MainAPI) {
apis = apis.filter { it != plugin }
synchronized(apis) {
apis = apis.filter { it != plugin }
}
initMap(true)
}
private fun initMap(forcedUpdate: Boolean = false) {
if (apiMap == null || forcedUpdate)
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
synchronized(apis) {
if (apiMap == null || forcedUpdate)
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
}
}
fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null
synchronized(allProviders) {
initMap()
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
// Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it.name == apiName }
synchronized(apis) {
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
// Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it.name == apiName }
}
}
}
@ -215,7 +225,7 @@ object APIHolder {
val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings()
val hasUniversal = activeLangs.contains(AllLanguagesName)
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
.map { it.name })
/*val set = settingsManager.getStringSet(
@ -314,8 +324,9 @@ object APIHolder {
} ?: default
val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName)
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
val allApis = synchronized(apis) {
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
}
return if (currentPrefMedia.isEmpty()) {
allApis
} else {
@ -736,6 +747,7 @@ fun fixTitle(str: String): String {
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
}
}
/**
* Get rhino context in a safe way as it needs to be initialized on the main thread.
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
@ -1122,7 +1134,7 @@ interface LoadResponse {
var isTrailersEnabled = true
fun LoadResponse.isMovie(): Boolean {
return this.type.isMovieType()
return this.type.isMovieType() || this is MovieLoadResponse
}
@JvmName("addActorNames")

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3
import android.animation.ValueAnimator
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@ -14,13 +15,17 @@ import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
@ -40,6 +45,7 @@ import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.Session
import com.google.android.gms.cast.framework.SessionManager
import com.google.android.gms.cast.framework.SessionManagerListener
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar
@ -58,6 +64,11 @@ import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observeNullable
@ -69,7 +80,7 @@ import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
@ -85,6 +96,7 @@ import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
@ -97,12 +109,14 @@ import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
import com.lagradost.cloudstream3.utils.ApkInstaller
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.isLtr
import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
@ -121,32 +135,19 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import kotlinx.android.synthetic.main.activity_main.cast_mini_controller_holder
import kotlinx.android.synthetic.main.activity_main.nav_host_fragment
import kotlinx.android.synthetic.main.activity_main.nav_rail_view
import kotlinx.android.synthetic.main.activity_main.nav_view
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_description
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_loading
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_loading_shimmer
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_duration
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_rating
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_type
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_year
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_more_info
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_poster
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_result
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_title
import kotlinx.android.synthetic.main.fragment_result_swipe.media_route_button
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.lang.ref.WeakReference
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.Charset
import kotlin.math.absoluteValue
import kotlin.reflect.KClass
import kotlin.system.exitProcess
@ -334,7 +335,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this@with.runOnUiThread {
try {
showToast(
this@with,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
@ -362,8 +362,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// Use both navigation views to support both layouts.
// It might be better to use the QuickSearch.
nav_view?.selectedItemId = R.id.navigation_search
nav_rail_view?.selectedItemId = R.id.navigation_search
activity?.findViewById<BottomNavigationView>(R.id.nav_view)?.selectedItemId =
R.id.navigation_search
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringPlayer) {
val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
@ -396,10 +398,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.navigate(R.id.navigation_downloads)
return true
} else {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
return true
synchronized(apis) {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
return true
}
}
}
}
@ -440,7 +444,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.hideKeyboard()
// Fucks up anime info layout since that has its own layout
cast_mini_controller_holder?.isVisible =
binding?.castMiniControllerHolder?.isVisible =
!listOf(
R.id.navigation_results_phone,
R.id.navigation_results_tv,
@ -476,15 +480,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_player,
).contains(destination.id)
nav_host_fragment?.apply {
binding?.navHostFragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams
val push =
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
if (!this.isLtr()) {
params.setMargins(
params.leftMargin,
params.topMargin,
push,
params.bottomMargin
)
} else {
params.setMargins(
push,
params.topMargin,
params.rightMargin,
params.bottomMargin
)
}
params.setMargins(
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
params.topMargin,
params.rightMargin,
params.bottomMargin
)
layoutParams = params
}
@ -494,21 +510,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
Configuration.ORIENTATION_PORTRAIT -> {
false
isTvSettings()
}
else -> {
false
}
}
binding?.apply {
navView.isVisible = isNavVisible && !landscape
navRailView.isVisible = isNavVisible && landscape
nav_view?.isVisible = isNavVisible && !landscape
nav_rail_view?.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
}
}
//private var mCastSession: CastSession? = null
@ -576,11 +593,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
CommonActivity.dispatchKeyEvent(this, event)?.let {
return it
val start = System.currentTimeMillis()
try {
val response = CommonActivity.dispatchKeyEvent(this, event)
if (response != null)
return response
} finally {
debugAssert({
val end = System.currentTimeMillis()
val delta = end - start
delta > 100
}) {
"Took over 100ms to navigate, smth is VERY wrong"
}
}
return super.dispatchKeyEvent(event)
}
@ -685,27 +714,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe {
pluginsLock.withLock {
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
allProviders.add(it.javaClass.newInstance().apply {
name = custom.name
lang = custom.lang
mainUrl = custom.url.trimEnd('/')
canBeOverridden = false
})
}
synchronized(allProviders) {
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
list.forEach { custom ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
?.let {
allProviders.add(it.javaClass.newInstance().apply {
name = custom.name
lang = custom.lang
mainUrl = custom.url.trimEnd('/')
canBeOverridden = false
})
}
}
}
// it.hashCode() is not enough to make sure they are distinct
apis =
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
APIHolder.apiMap = null
} catch (e: Exception) {
logError(e)
}
// it.hashCode() is not enough to make sure they are distinct
apis =
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
APIHolder.apiMap = null
} catch (e: Exception) {
logError(e)
}
}
}
@ -721,28 +752,217 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
private fun hidePreviewPopupDialog() {
viewModel.clear()
bottomPreviewPopup.dismissSafe(this)
bottomPreviewPopup = null
bottomPreviewBinding = null
}
var bottomPreviewPopup: BottomSheetDialog? = null
private fun showPreviewPopupDialog(): BottomSheetDialog {
val ret = (bottomPreviewPopup ?: run {
private var bottomPreviewPopup: BottomSheetDialog? = null
private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null
private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding {
val ret = (bottomPreviewBinding ?: run {
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_resultview_preview)
val binding: BottomResultviewPreviewBinding =
BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false)
bottomPreviewBinding = binding
builder.setContentView(binding.root)
builder.setOnDismissListener {
bottomPreviewPopup = null
bottomPreviewBinding = null
viewModel.clear()
}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
bottomPreviewPopup = builder
binding
})
bottomPreviewPopup = ret
return ret
}
private var binding: ActivityMainBinding? = null
object TvFocus {
data class FocusTarget(
val width: Int,
val height: Int,
val x: Float,
val y: Float,
) {
companion object {
fun lerp(a: FocusTarget, b: FocusTarget, lerp: Float): FocusTarget {
val ilerp = 1 - lerp
return FocusTarget(
width = (a.width * ilerp + b.width * lerp).toInt(),
height = (a.height * ilerp + b.height * lerp).toInt(),
x = a.x * ilerp + b.x * lerp,
y = a.y * ilerp + b.y * lerp
)
}
}
}
var last: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
var current: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
var focusOutline: WeakReference<View> = WeakReference(null)
var lastFocus: WeakReference<View> = WeakReference(null)
private val layoutListener: View.OnLayoutChangeListener =
View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
updateFocusView(
v, same = true
)
}
private val attachListener: View.OnAttachStateChangeListener =
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
updateFocusView(v)
}
override fun onViewDetachedFromWindow(v: View) {
// removes the focus view but not the listener as updateFocusView(null) will remove the listener
focusOutline.get()?.isVisible = false
}
}
private fun setTargetPosition(target: FocusTarget) {
focusOutline.get()?.apply {
layoutParams = layoutParams?.apply {
width = target.width
height = target.height
}
translationX = target.x
translationY = target.y
bringToFront()
}
}
private var animator: ValueAnimator? = null
@MainThread
fun updateFocusView(newFocus: View?, same: Boolean = false) {
val focusOutline = focusOutline.get() ?: return
lastFocus.get()?.apply {
removeOnLayoutChangeListener(layoutListener)
removeOnAttachStateChangeListener(attachListener)
}
val wasGone = focusOutline.isGone
val visible =
newFocus != null && newFocus.measuredHeight > 0 && newFocus.measuredWidth > 0 && newFocus.isShown && newFocus.tag != "tv_no_focus_tag"
focusOutline.isVisible = visible
if (newFocus != null) {
lastFocus = WeakReference(newFocus)
val out = IntArray(2)
newFocus.getLocationInWindow(out)
val (screenX, screenY) = out
var (x, y) = screenX.toFloat() to screenY.toFloat()
val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY
// println(">><<< $x $y $currentX $currentY")
if (!newFocus.isLtr()) {
x = x - focusOutline.rootView.width + newFocus.measuredWidth
}
// out of bounds = 0,0
if (screenX == 0 && screenY == 0) {
focusOutline.isVisible = false
}
newFocus.addOnLayoutChangeListener(layoutListener)
newFocus.addOnAttachStateChangeListener(attachListener)
val start = FocusTarget(
x = currentX,
y = currentY,
width = focusOutline.measuredWidth,
height = focusOutline.measuredHeight
)
val end = FocusTarget(
x = x,
y = y,
width = newFocus.measuredWidth,
height = newFocus.measuredHeight
)
// if they are the same within then snap, aka scrolling
val deltaMin = 50.toPx
if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) {
animator?.cancel()
last = start
current = end
setTargetPosition(end)
return
}
// if running then "reuse"
if (animator?.isRunning == true) {
current = end
return
} else {
animator?.cancel()
}
last = start
current = end
// if previously gone, then tp
if (wasGone) {
setTargetPosition(current)
return
}
// animate between a and b
animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
startDelay = 0
duration = 100
addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float
val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
setTargetPosition(target)
}
start()
}
// post check
if (!same) {
newFocus.postDelayed({
updateFocusView(lastFocus.get(), same = true)
}, 200)
}
/*
the following is working, but somewhat bad code code
if (!wasGone) {
(focusOutline.parent as? ViewGroup)?.let {
TransitionManager.endTransitions(it)
TransitionManager.beginDelayedTransition(
it,
TransitionSet().addTransition(ChangeBounds())
.addTransition(ChangeTransform())
.setDuration(100)
)
}
}
focusOutline.layoutParams = focusOutline.layoutParams?.apply {
width = newFocus.measuredWidth
height = newFocus.measuredHeight
}
focusOutline.translationX = x.toFloat()
focusOutline.translationY = y.toFloat()*/
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -767,16 +987,48 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (isCastApiAvailable()) {
mSessionManager = CastContext.getSharedInstance(this).sessionManager
}
} catch (e: Exception) {
logError(e)
} catch (t: Throwable) {
logError(t)
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
updateTv()
if (isTvSettings()) {
setContentView(R.layout.activity_main_tv)
} else {
setContentView(R.layout.activity_main)
// backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
try {
val appVer = BuildConfig.VERSION_NAME
val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: ""
if (appVer != lastAppAutoBackup) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
backup()
}
} catch (t: Throwable) {
logError(t)
}
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
binding = try {
if (isTvSettings()) {
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root)
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
// println("refocus $oldFocus -> $newFocus")
TvFocus.updateFocusView(newFocus)
}
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
}
ActivityMainBinding.bind(newLocalBinding.root) // this may crash
} else {
val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root)
newLocalBinding
}
} catch (t: Throwable) {
showToast(txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG)
null
}
changeStatusBarState(isEmulatorSettings())
@ -807,7 +1059,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
showToast(R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} else if (lastError == null) {
ioSafe {
@ -861,43 +1113,44 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
observeNullable(viewModel.page) { resource ->
if (resource == null) {
bottomPreviewPopup.dismissSafe(this)
hidePreviewPopupDialog()
return@observeNullable
}
when (resource) {
is Resource.Failure -> {
showToast(this, R.string.error)
showToast(R.string.error)
viewModel.clear()
hidePreviewPopupDialog()
}
is Resource.Loading -> {
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = true
resultview_preview_result?.isVisible = false
resultview_preview_loading_shimmer?.startShimmer()
resultviewPreviewLoading.isVisible = true
resultviewPreviewResult.isVisible = false
resultviewPreviewLoadingShimmer.startShimmer()
}
}
is Resource.Success -> {
val d = resource.value
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = false
resultview_preview_result?.isVisible = true
resultview_preview_loading_shimmer?.stopShimmer()
resultviewPreviewLoading.isVisible = false
resultviewPreviewResult.isVisible = true
resultviewPreviewLoadingShimmer.stopShimmer()
resultview_preview_title?.text = d.title
resultviewPreviewTitle.text = d.title
resultview_preview_meta_type.setText(d.typeText)
resultview_preview_meta_year.setText(d.yearText)
resultview_preview_meta_duration.setText(d.durationText)
resultview_preview_meta_rating.setText(d.ratingText)
resultviewPreviewMetaType.setText(d.typeText)
resultviewPreviewMetaYear.setText(d.yearText)
resultviewPreviewMetaDuration.setText(d.durationText)
resultviewPreviewMetaRating.setText(d.ratingText)
resultview_preview_description?.setText(d.plotText)
resultview_preview_poster?.setImage(
resultviewPreviewDescription.setText(d.plotText)
resultviewPreviewPoster.setImage(
d.posterImage ?: d.posterBackgroundImage
)
resultview_preview_poster?.setOnClickListener {
resultviewPreviewPoster.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
val value = viewModel.watchStatus.value ?: WatchType.NONE
@ -908,12 +1161,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
bookmarksUpdatedEvent(true)
}
}
if (!isTvSettings()) // dont want this clickable on tv layout
resultview_preview_description?.setOnClickListener { view ->
resultviewPreviewDescription.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
@ -923,7 +1175,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
resultview_preview_more_info?.setOnClickListener {
resultviewPreviewMoreInfo.setOnClickListener {
viewModel.clear()
hidePreviewPopupDialog()
lastPopup?.let {
loadSearchResult(it)
@ -964,7 +1217,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
ioSafe {
initAll()
// No duplicates (which can happen by registerMainAPI)
apis = allProviders.distinctBy { it }
apis = synchronized(allProviders) {
allProviders.distinctBy { it }
}
}
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
@ -977,6 +1232,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
// Intercept search and add a query
updateNavBar(navDestination)
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
bundle?.apply {
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
@ -995,29 +1251,47 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
.setPopExitAnim(R.anim.nav_pop_exit)
.setPopUpTo(navController.graph.startDestination, false)
.build()*/
nav_view?.setupWithNavController(navController)
val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
nav_rail?.setupWithNavController(navController)
if (isTvSettings()) {
nav_rail?.background?.alpha = 200
} else {
nav_rail?.background?.alpha = 255
val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
binding?.navView?.apply {
itemRippleColor = rippleColor
itemActiveIndicatorColor = rippleColor
setupWithNavController(navController)
setOnItemSelectedListener { item ->
onNavDestinationSelected(
item,
navController
)
}
}
nav_rail?.setOnItemSelectedListener { item ->
onNavDestinationSelected(
item,
navController
)
}
nav_view?.setOnItemSelectedListener { item ->
onNavDestinationSelected(
item,
navController
)
}
navController.addOnDestinationChangedListener { _, destination, _ ->
updateNavBar(destination)
binding?.navRailView?.apply {
itemRippleColor = rippleColor
itemActiveIndicatorColor = rippleColor
setupWithNavController(navController)
if (isTvSettings()) {
background?.alpha = 200
} else {
background?.alpha = 255
}
setOnItemSelectedListener { item ->
onNavDestinationSelected(
item,
navController
)
}
fun noFocus(view: View) {
view.tag = view.context.getString(R.string.tv_no_focus_tag)
(view as? ViewGroup)?.let {
for (child in it.children) {
noFocus(child)
}
}
}
noFocus(this)
}
loadCache()
@ -1040,17 +1314,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
true
}*/
val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
nav_view?.itemRippleColor = rippleColor
nav_rail?.itemRippleColor = rippleColor
nav_rail?.itemActiveIndicatorColor = rippleColor
nav_view?.itemActiveIndicatorColor = rippleColor
if (!checkWrite()) {
requestRW()
if (checkWrite()) return
}
CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
//CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
// THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION
//if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) {
@ -1117,14 +1386,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n"
for (api in allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix(
"https://"
)
}\" android:pathPrefix=\"/\"/>\n"
synchronized(allProviders) {
for (api in allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix(
"https://"
)
}\" android:pathPrefix=\"/\"/>\n"
}
}
println(providersAndroidManifestString)
}

View file

@ -0,0 +1,42 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.getAndUnpack
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.M3u8Helper
open class StreamoUpload : ExtractorApi() {
override val name = "StreamoUpload"
override val mainUrl = "https://streamoupload.xyz"
override val requiresReferer = true
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
val sources = mutableListOf<ExtractorLink>()
val response = app.get(url, referer = referer)
val scriptElements = response.document.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val data = getAndUnpack(script.data())
.substringAfter("sources:[")
.substringBefore("],")
.replace("file", "\"file\"")
.trim()
tryParseJson<File>(data)?.let {
M3u8Helper.generateM3u8(
name,
it.file,
"$mainUrl/",
).forEach { m3uData -> sources.add(m3uData) }
}
}
}
return sources
}
private data class File(
@JsonProperty("file") val file: String,
)
}

View file

@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
}
private val validApis by lazy {
apis.filter { it.lang == this.lang && it::class.java != this::class.java }
//.distinctBy { it.uniqueId }
}
private val validApis
get() =
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
//.distinctBy { it.uniqueId }
data class CrossMetaData(
@JsonProperty("isSuccess") val isSuccess: Boolean,
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
override suspend fun load(url: String): LoadResponse? {
val base = super.load(url)?.apply {
this.recommendations = this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
this.recommendations =
this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
val matchName = filterName(this.name)
when (this) {
is MovieLoadResponse -> {
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
this.dataUrl =
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
}
else -> {
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
}

View file

@ -25,13 +25,16 @@ class MultiAnimeProvider : MainAPI() {
}
}
private val validApis by lazy {
APIHolder.apis.filter {
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
TvType.Anime
)
}
}
private val validApis
get() =
synchronized(APIHolder.apis) {
APIHolder.apis.filter {
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
TvType.Anime
)
}
}
private fun filterName(name: String): String {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")

View file

@ -57,32 +57,6 @@ fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) ->
liveData.observe(this) { action(it) }
}
inline fun <reified T : Any> some(value: T?): Some<T> {
return if (value == null) {
Some.None
} else {
Some.Success(value)
}
}
sealed class Some<out T> {
data class Success<out T>(val value: T) : Some<T>()
object None : Some<Nothing>()
override fun toString(): String {
return when (this) {
is None -> "None"
is Success -> "Some(${value.toString()})"
}
}
}
sealed class ResourceSome<out T> {
data class Success<out T>(val value: T) : ResourceSome<T>()
object None : ResourceSome<Nothing>()
data class Loading(val data: Any? = null) : ResourceSome<Nothing>()
}
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(
@ -155,6 +129,70 @@ fun CoroutineScope.launchSafe(
return this.launch(context, start, obj)
}
fun<T> throwAbleToResource(
throwable: Throwable
): Resource<T> {
return when (throwable) {
is NullPointerException -> {
for (line in throwable.stackTrace) {
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
return Resource.Failure(
false,
null,
null,
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
)
}
}
safeFail(throwable)
}
is SocketTimeoutException, is InterruptedIOException -> {
Resource.Failure(
true,
null,
null,
"Connection Timeout\nPlease try again later."
)
}
is HttpException -> {
Resource.Failure(
false,
throwable.statusCode,
null,
throwable.message ?: "HttpException"
)
}
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
}
is ErrorLoadingException -> {
Resource.Failure(
true,
null,
null,
throwable.message ?: "Error loading, try again later."
)
}
is NotImplementedError -> {
Resource.Failure(false, null, null, "This operation is not implemented.")
}
is SSLHandshakeException -> {
Resource.Failure(
true,
null,
null,
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
)
}
is CancellationException -> {
throwable.cause?.let {
throwAbleToResource(it)
} ?: safeFail(throwable)
}
else -> safeFail(throwable)
}
}
suspend fun <T> safeApiCall(
apiCall: suspend () -> T,
): Resource<T> {
@ -163,60 +201,7 @@ suspend fun <T> safeApiCall(
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
logError(throwable)
when (throwable) {
is NullPointerException -> {
for (line in throwable.stackTrace) {
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
return@withContext Resource.Failure(
false,
null,
null,
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
)
}
}
safeFail(throwable)
}
is SocketTimeoutException, is InterruptedIOException -> {
Resource.Failure(
true,
null,
null,
"Connection Timeout\nPlease try again later."
)
}
is HttpException -> {
Resource.Failure(
false,
throwable.statusCode,
null,
throwable.message ?: "HttpException"
)
}
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.")
}
is ErrorLoadingException -> {
Resource.Failure(
true,
null,
null,
throwable.message ?: "Error loading, try again later."
)
}
is NotImplementedError -> {
Resource.Failure(false, null, null, "This operation is not implemented.")
}
is SSLHandshakeException -> {
Resource.Failure(
true,
null,
null,
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
)
}
else -> safeFail(throwable)
}
throwAbleToResource(throwable)
}
}
}

View file

@ -36,7 +36,9 @@ abstract class Plugin {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
element.sourcePlugin = this.__filename
// Race condition causing which would case duplicates if not for distinctBy
APIHolder.allProviders.add(element)
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.add(element)
}
APIHolder.addPluginMapping(element)
}
@ -51,10 +53,14 @@ abstract class Plugin {
}
class Manifest {
@JsonProperty("name") var name: String? = null
@JsonProperty("pluginClassName") var pluginClassName: String? = null
@JsonProperty("version") var version: Int? = null
@JsonProperty("requiresResources") var requiresResources: Boolean = false
@JsonProperty("name")
var name: String? = null
@JsonProperty("pluginClassName")
var pluginClassName: String? = null
@JsonProperty("version")
var version: Int? = null
@JsonProperty("requiresResources")
var requiresResources: Boolean = false
}
/**

View file

@ -163,7 +163,8 @@ object PluginManager {
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
HashMap<PathClassLoader, Plugin>()
private var loadedLocalPlugins = false
var loadedLocalPlugins = false
private set
private val gson = Gson()
private suspend fun maybeLoadPlugin(context: Context, file: File) {
@ -531,10 +532,14 @@ object PluginManager {
}
// remove all registered apis
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
removePluginMapping(it)
synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
removePluginMapping(it)
}
}
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
}
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
classLoaders.values.removeIf { v -> v == plugin }

View file

@ -24,7 +24,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
}
}
override fun onRequestChildFocus(
/*override fun onRequestChildFocus(
parent: RecyclerView,
state: RecyclerView.State,
child: View,
@ -32,13 +32,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
): Boolean {
// android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams
return try {
val pos = maxOf(0, getPosition(focused!!) - 2)
parent.scrollToPosition(pos)
if(focused != null) {
// val pos = maxOf(0, getPosition(focused) - 2) // IDK WHY
val pos = getPosition(focused)
if(pos >= 0) parent.scrollToPosition(pos)
}
super.onRequestChildFocus(parent, state, child, focused)
} catch (e: Exception) {
false
}
}
}*/
// Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d
override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
@ -65,8 +69,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
val spanCount = this.spanCount
val orientation = this.orientation
// fixes arabic by inverting left and right layout focus
val correctDirection = if(this.isLayoutRTL) {
when(direction) {
View.FOCUS_RIGHT -> View.FOCUS_LEFT
View.FOCUS_LEFT -> View.FOCUS_RIGHT
else -> direction
}
} else direction
if (orientation == VERTICAL) {
when (direction) {
when (correctDirection) {
View.FOCUS_DOWN -> {
return spanCount
}
@ -81,7 +94,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
}
}
} else if (orientation == HORIZONTAL) {
when (direction) {
when (correctDirection) {
View.FOCUS_DOWN -> {
return 1
}

View file

@ -16,14 +16,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.R
import kotlinx.android.synthetic.main.activity_easter_egg_monke.*
import java.util.*
import com.lagradost.cloudstream3.databinding.ActivityEasterEggMonkeBinding
class EasterEggMonke : AppCompatActivity() {
lateinit var binding : ActivityEasterEggMonkeBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_easter_egg_monke)
binding = ActivityEasterEggMonkeBinding.inflate(layoutInflater)
setContentView(binding.root)
val handler = Handler(mainLooper)
lateinit var runnable: Runnable
@ -32,15 +34,14 @@ class EasterEggMonke : AppCompatActivity() {
handler.postDelayed(runnable, 300)
}
handler.postDelayed(runnable, 1000)
}
private fun shower() {
val containerW = frame.width
val containerH = frame.height
var starW: Float = monke.width.toFloat()
var starH: Float = monke.height.toFloat()
val containerW = binding.frame.width
val containerH = binding.frame.height
var starW: Float = binding.monke.width.toFloat()
var starH: Float = binding.monke.height.toFloat()
val newStar = AppCompatImageView(this)
val idx = (monkeys.size * Math.random()).toInt()
@ -48,7 +49,7 @@ class EasterEggMonke : AppCompatActivity() {
newStar.isVisible = true
newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT)
frame.addView(newStar)
binding.frame.addView(newStar)
newStar.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX
newStar.scaleY = newStar.scaleX
@ -70,7 +71,7 @@ class EasterEggMonke : AppCompatActivity() {
set.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
frame.removeView(newStar)
binding.frame.removeView(newStar)
}
})

View file

@ -12,20 +12,23 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding
import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import kotlinx.android.synthetic.main.fragment_webview.*
class WebviewFragment : Fragment() {
var binding: FragmentWebviewBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val url = arguments?.getString(WEBVIEW_URL) ?: "".also {
findNavController().popBackStack()
}
web_view.webViewClient = object : WebViewClient() {
binding?.webView?.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
@ -40,24 +43,28 @@ class WebviewFragment : Fragment() {
return super.shouldOverrideUrlLoading(view, request)
}
}
binding?.webView?.apply {
WebViewResolver.webViewUserAgent = settings.userAgentString
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
web_view.settings.javaScriptEnabled = true
web_view.settings.userAgentString = USER_AGENT
web_view.settings.domStorageEnabled = true
addJavascriptInterface(RepoApi(activity), "RepoApi")
settings.javaScriptEnabled = true
settings.userAgentString = USER_AGENT
settings.domStorageEnabled = true
// WebView.setWebContentsDebuggingEnabled(true)
web_view.loadUrl(url)
loadUrl(url)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
val localBinding = FragmentWebviewBinding.inflate(inflater, container, false)
binding = localBinding
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_webview, container, false)
return localBinding.root//inflater.inflate(R.layout.fragment_webview, container, false)
}
companion object {
@ -70,7 +77,7 @@ class WebviewFragment : Fragment() {
private class RepoApi(val activity: FragmentActivity?) {
@JavascriptInterface
fun installRepo(repoUrl: String) {
fun installRepo(repoUrl: String) {
activity?.loadRepository(repoUrl)
}
}

View file

@ -5,6 +5,7 @@ import android.content.DialogInterface
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError
@ -19,7 +20,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
object DownloadButtonSetup {
fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) {
fun handleDownloadClick(click: DownloadClickEvent) {
val id = click.data.id
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
when (click.action) {
@ -89,9 +90,9 @@ object DownloadButtonSetup {
)?.fileLength
?: 0
if (length > 0) {
showToast(act, R.string.delete, Toast.LENGTH_LONG)
showToast(R.string.delete, Toast.LENGTH_LONG)
} else {
showToast(act, R.string.download, Toast.LENGTH_LONG)
showToast(R.string.download, Toast.LENGTH_LONG)
}
}
}

View file

@ -1,6 +0,0 @@
package com.lagradost.cloudstream3.ui.download
interface DownloadButtonViewHolder {
var downloadButton : EasyDownloadButton
fun reattachDownloadButton()
}

View file

@ -3,18 +3,12 @@ package com.lagradost.cloudstream3.ui.download
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.widget.ContentLoadingProgressBar
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.android.synthetic.main.download_child_episode.view.*
import java.util.*
const val DOWNLOAD_ACTION_PLAY_FILE = 0
const val DOWNLOAD_ACTION_DELETE_FILE = 1
@ -36,39 +30,9 @@ class DownloadChildAdapter(
private val clickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
return Collections.unmodifiableSet(mBoundViewHolders)
}
fun killAdapter() {
getAllBoundViewHolders()?.forEach { view ->
view?.downloadButton?.dispose()
}
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
holder.downloadButton.dispose()
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
holder.downloadButton.dispose()
mBoundViewHolders.remove(holder)
}
}
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
holder.reattachDownloadButton()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DownloadChildViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.download_child_episode, parent, false),
DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false),
clickCallback
)
}
@ -77,7 +41,6 @@ class DownloadChildAdapter(
when (holder) {
is DownloadChildViewHolder -> {
holder.bind(cardList[position])
mBoundViewHolders.add(holder)
}
}
}
@ -88,66 +51,44 @@ class DownloadChildAdapter(
class DownloadChildViewHolder
constructor(
itemView: View,
val binding: DownloadChildEpisodeBinding,
private val clickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
override var downloadButton = EasyDownloadButton()
) : RecyclerView.ViewHolder(binding.root) {
private val title: TextView = itemView.download_child_episode_text
/*private val title: TextView = itemView.download_child_episode_text
private val extraInfo: TextView = itemView.download_child_episode_text_extra
private val holder: CardView = itemView.download_child_episode_holder
private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress
private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded
private val downloadImage: ImageView = itemView.download_child_episode_download
private val downloadImage: ImageView = itemView.download_child_episode_download*/
private var localCard: VisualDownloadChildCached? = null
fun bind(card: VisualDownloadChildCached) {
localCard = card
val d = card.data
val posDur = getViewPos(d.id)
if (posDur != null) {
val visualPos = posDur.fixVisual()
progressBar.max = (visualPos.duration / 1000).toInt()
progressBar.progress = (visualPos.position / 1000).toInt()
progressBar.visibility = View.VISIBLE
} else {
progressBar.visibility = View.GONE
binding.downloadChildEpisodeProgress.apply {
if (posDur != null) {
val visualPos = posDur.fixVisual()
max = (visualPos.duration / 1000).toInt()
progress = (visualPos.position / 1000).toInt()
visibility = View.VISIBLE
} else {
visibility = View.GONE
}
}
title.text = title.context.getNameFull(d.name, d.episode, d.season)
title.isSelected = true // is needed for text repeating
binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback)
downloadButton.setUpButton(
card.currentBytes,
card.totalBytes,
progressBarDownload,
downloadImage,
extraInfo,
card.data,
clickCallback
)
binding.downloadChildEpisodeText.apply {
text = context.getNameFull(d.name, d.episode, d.season)
isSelected = true // is needed for text repeating
}
holder.setOnClickListener {
binding.downloadChildEpisodeHolder.setOnClickListener {
clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
}
}
override fun reattachDownloadButton() {
downloadButton.dispose()
val card = localCard
if (card != null) {
downloadButton.setUpButton(
card.currentBytes,
card.totalBytes,
progressBarDownload,
downloadImage,
extraInfo,
card.data,
clickCallback
)
}
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -15,13 +16,12 @@ import com.lagradost.cloudstream3.utils.DataStore.getKeys
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.android.synthetic.main.fragment_child_downloads.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DownloadChildFragment : Fragment() {
companion object {
fun newInstance(headerName: String, folder: String) : Bundle {
fun newInstance(headerName: String, folder: String): Bundle {
return Bundle().apply {
putString("folder", folder)
putString("name", headerName)
@ -30,13 +30,20 @@ class DownloadChildFragment : Fragment() {
}
override fun onDestroyView() {
(download_child_list?.adapter as DownloadChildAdapter?)?.killAdapter()
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
binding = null
super.onDestroyView()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_child_downloads, container, false)
var binding: FragmentChildDownloadsBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false)
}
private fun updateList(folder: String) = main {
@ -50,14 +57,15 @@ class DownloadChildFragment : Fragment() {
?: return@mapNotNull null
VisualDownloadChildCached(info.fileLength, info.totalBytes, it)
}
}.sortedBy { it.data.episode + (it.data.season?: 0)*100000 }
}.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 }
if (eps.isEmpty()) {
activity?.onBackPressed()
return@main
}
(download_child_list?.adapter as DownloadChildAdapter? ?: return@main).cardList = eps
download_child_list?.adapter?.notifyDataSetChanged()
(binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList =
eps
binding?.downloadChildList?.adapter?.notifyDataSetChanged()
}
}
@ -72,23 +80,26 @@ class DownloadChildFragment : Fragment() {
activity?.onBackPressed() // TODO FIX
return
}
context?.fixPaddingStatusbar(download_child_root)
fixPaddingStatusbar(binding?.downloadChildRoot)
download_child_toolbar.title = name
download_child_toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
download_child_toolbar.setNavigationOnClickListener {
activity?.onBackPressed()
binding?.downloadChildToolbar?.apply {
title = name
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener {
activity?.onBackPressed()
}
}
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
DownloadChildAdapter(
ArrayList(),
) { click ->
handleDownloadClick(activity, click)
handleDownloadClick(click)
}
downloadDeleteEventListener = { id: Int ->
val list = (download_child_list?.adapter as DownloadChildAdapter?)?.cardList
val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList
if (list != null) {
if (list.any { it.data.id == id }) {
updateList(folder)
@ -98,8 +109,8 @@ class DownloadChildFragment : Fragment() {
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
download_child_list.adapter = adapter
download_child_list.layoutManager = GridLayoutManager(context, 1)
binding?.downloadChildList?.adapter = adapter
binding?.downloadChildList?.layoutManager = GridLayoutManager(context, 1)
updateList(folder)
}

View file

@ -34,10 +34,10 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.android.synthetic.main.fragment_downloads.*
import kotlinx.android.synthetic.main.stream_input.*
import android.text.format.Formatter.formatShortFileSize
import androidx.core.widget.doOnTextChanged
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
import com.lagradost.cloudstream3.databinding.StreamInputBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@ -60,8 +60,8 @@ class DownloadFragment : Fragment() {
private fun setList(list: List<VisualDownloadHeaderCached>) {
main {
(download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list
download_list?.adapter?.notifyDataSetChanged()
(binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list
binding?.downloadList?.adapter?.notifyDataSetChanged()
}
}
@ -70,10 +70,12 @@ class DownloadFragment : Fragment() {
VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!!
downloadDeleteEventListener = null
}
(download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter()
binding = null
super.onDestroyView()
}
var binding : FragmentDownloadsBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -82,7 +84,9 @@ class DownloadFragment : Fragment() {
downloadsViewModel =
ViewModelProvider(this)[DownloadViewModel::class.java]
return inflater.inflate(R.layout.fragment_downloads, container, false)
val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false)
}
private var downloadDeleteEventListener: ((Int) -> Unit)? = null
@ -92,36 +96,40 @@ class DownloadFragment : Fragment() {
hideKeyboard()
observe(downloadsViewModel.noDownloadsText) {
text_no_downloads.text = it
binding?.textNoDownloads?.text = it
}
observe(downloadsViewModel.headerCards) {
setList(it)
download_loading.isVisible = false
binding?.downloadLoading?.isVisible = false
}
observe(downloadsViewModel.availableBytes) {
download_free_txt?.text =
binding?.downloadFreeTxt?.text =
getString(R.string.storage_size_format).format(
getString(R.string.free_storage),
formatShortFileSize(view.context, it)
)
download_free?.setLayoutWidth(it)
binding?.downloadFree?.setLayoutWidth(it)
}
observe(downloadsViewModel.usedBytes) {
download_used_txt?.text =
getString(R.string.storage_size_format).format(
getString(R.string.used_storage),
formatShortFileSize(view.context, it)
)
download_used?.setLayoutWidth(it)
download_storage_appbar?.isVisible = it > 0
binding?.apply {
downloadUsedTxt.text =
getString(R.string.storage_size_format).format(
getString(R.string.used_storage),
formatShortFileSize(view.context, it)
)
downloadUsed.setLayoutWidth(it)
downloadStorageAppbar.isVisible = it > 0
}
}
observe(downloadsViewModel.downloadBytes) {
download_app_txt?.text =
getString(R.string.storage_size_format).format(
getString(R.string.app_storage),
formatShortFileSize(view.context, it)
)
download_app?.setLayoutWidth(it)
binding?.apply {
downloadAppTxt.text =
getString(R.string.storage_size_format).format(
getString(R.string.app_storage),
formatShortFileSize(view.context, it)
)
downloadApp.setLayoutWidth(it)
}
}
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
@ -154,7 +162,7 @@ class DownloadFragment : Fragment() {
},
{ downloadClickEvent ->
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
handleDownloadClick(activity, downloadClickEvent)
handleDownloadClick(downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
context?.let { ctx ->
downloadsViewModel.updateList(ctx)
@ -164,7 +172,7 @@ class DownloadFragment : Fragment() {
)
downloadDeleteEventListener = { id ->
val list = (download_list?.adapter as DownloadHeaderAdapter?)?.cardList
val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList
if (list != null) {
if (list.any { it.data.id == id }) {
context?.let { ctx ->
@ -177,31 +185,36 @@ class DownloadFragment : Fragment() {
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
download_list?.adapter = adapter
download_list?.layoutManager = GridLayoutManager(context, 1)
binding?.downloadList?.apply {
this.adapter = adapter
layoutManager = GridLayoutManager(context, 1)
}
// Should be visible in emulator layout
download_stream_button?.isGone = isTrueTvSettings()
download_stream_button?.setOnClickListener {
binding?.downloadStreamButton?.isGone = isTrueTvSettings()
binding?.downloadStreamButton?.setOnClickListener {
val dialog =
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)
dialog.setContentView(R.layout.stream_input)
val binding = StreamInputBinding.inflate(dialog.layoutInflater)
dialog.setContentView(binding.root)
dialog.show()
// If user has clicked the switch do not interfere
var preventAutoSwitching = false
dialog.hls_switch?.setOnClickListener {
binding.hlsSwitch.setOnClickListener {
preventAutoSwitching = true
}
fun activateSwitchOnHls(text: String?) {
dialog.hls_switch?.isChecked = normalSafeApiCall {
binding.hlsSwitch.isChecked = normalSafeApiCall {
URI(text).path?.substringAfterLast(".")?.contains("m3u")
} == true
}
dialog.stream_referer?.doOnTextChanged { text, _, _, _ ->
binding.streamReferer.doOnTextChanged { text, _, _, _ ->
if (!preventAutoSwitching)
activateSwitchOnHls(text?.toString())
}
@ -210,16 +223,16 @@ class DownloadFragment : Fragment() {
0
)?.text?.toString()?.let { copy ->
val fixedText = copy.trim()
dialog.stream_url?.setText(fixedText)
binding.streamUrl.setText(fixedText)
activateSwitchOnHls(fixedText)
}
dialog.apply_btt?.setOnClickListener {
val url = dialog.stream_url.text?.toString()
binding.applyBtt.setOnClickListener {
val url = binding.streamUrl.text?.toString()
if (url.isNullOrEmpty()) {
showToast(activity, R.string.error_invalid_url, Toast.LENGTH_SHORT)
showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT)
} else {
val referer = dialog.stream_referer.text?.toString()
val referer = binding.streamReferer.text?.toString()
activity?.navigate(
R.id.global_to_navigation_player,
@ -228,7 +241,7 @@ class DownloadFragment : Fragment() {
listOf(BasicLink(url)),
extract = true,
referer = referer,
isM3u8 = dialog.hls_switch?.isChecked
isM3u8 = binding.hlsSwitch.isChecked
)
)
)
@ -237,22 +250,22 @@ class DownloadFragment : Fragment() {
}
}
dialog.cancel_btt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
download_list?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val dy = scrollY - oldScrollY
if (dy > 0) { //check for scroll down
download_stream_button?.shrink() // hide
binding?.downloadStreamButton?.shrink() // hide
} else if (dy < -5) {
download_stream_button?.extend() // show
binding?.downloadStreamButton?.extend() // show
}
}
}
downloadsViewModel.updateList(requireContext())
context?.fixPaddingStatusbar(download_root)
fixPaddingStatusbar(binding?.downloadRoot)
}
}

View file

@ -5,16 +5,13 @@ import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.widget.ContentLoadingProgressBar
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.android.synthetic.main.download_header_episode.view.*
import java.util.*
data class VisualDownloadHeaderCached(
@ -26,7 +23,10 @@ data class VisualDownloadHeaderCached(
val child: VideoDownloadHelper.DownloadEpisodeCached?,
)
data class DownloadHeaderClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadHeaderCached)
data class DownloadHeaderClickEvent(
val action: Int,
val data: VideoDownloadHelper.DownloadHeaderCached
)
class DownloadHeaderAdapter(
var cardList: List<VisualDownloadHeaderCached>,
@ -34,39 +34,13 @@ class DownloadHeaderAdapter(
private val movieClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
return Collections.unmodifiableSet(mBoundViewHolders)
}
fun killAdapter() {
getAllBoundViewHolders()?.forEach { view ->
view?.downloadButton?.dispose()
}
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
holder.downloadButton.dispose()
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
holder.downloadButton.dispose()
mBoundViewHolders.remove(holder)
}
}
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
holder.reattachDownloadButton()
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DownloadHeaderViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.download_header_episode, parent, false),
DownloadHeaderEpisodeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
clickCallback,
movieClickCallback
)
@ -76,7 +50,6 @@ class DownloadHeaderAdapter(
when (holder) {
is DownloadHeaderViewHolder -> {
holder.bind(cardList[position])
mBoundViewHolders.add(holder)
}
}
}
@ -87,93 +60,89 @@ class DownloadHeaderAdapter(
class DownloadHeaderViewHolder
constructor(
itemView: View,
val binding: DownloadHeaderEpisodeBinding,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val movieClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
override var downloadButton = EasyDownloadButton()
) : RecyclerView.ViewHolder(binding.root) {
private val poster: ImageView? = itemView.download_header_poster
/*private val poster: ImageView? = itemView.download_header_poster
private val title: TextView = itemView.download_header_title
private val extraInfo: TextView = itemView.download_header_info
private val holder: CardView = itemView.episode_holder
private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded
private val downloadImage: ImageView = itemView.download_header_episode_download
private val normalImage: ImageView = itemView.download_header_goto_child
private var localCard: VisualDownloadHeaderCached? = null
private val normalImage: ImageView = itemView.download_header_goto_child*/
@SuppressLint("SetTextI18n")
fun bind(card: VisualDownloadHeaderCached) {
localCard = card
val d = card.data
poster?.setImage(d.poster)
poster?.setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
binding.downloadHeaderPoster.apply {
setImage(d.poster)
setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
}
}
title.text = d.name
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
binding.apply {
//val isMovie = d.type.isMovieType()
if (card.child != null) {
downloadBar.visibility = View.VISIBLE
downloadImage.visibility = View.VISIBLE
normalImage.visibility = View.GONE
/*setUpButton(
card.currentBytes,
card.totalBytes,
downloadBar,
downloadImage,
extraInfo,
card.child,
movieClickCallback
)*/
binding.downloadHeaderTitle.text = d.name
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
holder.setOnClickListener {
movieClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child))
}
} else {
downloadBar.visibility = View.GONE
downloadImage.visibility = View.GONE
normalImage.visibility = View.VISIBLE
//val isMovie = d.type.isMovieType()
if (card.child != null) {
//downloadHeaderProgressDownloaded.visibility = View.VISIBLE
try {
extraInfo.text =
extraInfo.context.getString(R.string.extra_info_format).format(
card.totalDownloads,
if (card.totalDownloads == 1) extraInfo.context.getString(R.string.episode) else extraInfo.context.getString(
R.string.episodes
),
mbString
// downloadHeaderEpisodeDownload.visibility = View.VISIBLE
binding.downloadHeaderGotoChild.visibility = View.GONE
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback)
downloadButton.isVisible = true
/*setUpButton(
card.currentBytes,
card.totalBytes,
downloadBar,
downloadImage,
extraInfo,
card.child,
movieClickCallback
)*/
episodeHolder.setOnClickListener {
movieClickCallback.invoke(
DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
card.child
)
)
} catch (t : Throwable) {
// you probably formatted incorrectly
extraInfo.text = "Error"
logError(t)
}
} else {
downloadButton.isVisible = false
// downloadHeaderProgressDownloaded.visibility = View.GONE
// downloadHeaderEpisodeDownload.visibility = View.GONE
binding.downloadHeaderGotoChild.visibility = View.VISIBLE
try {
downloadHeaderInfo.text =
downloadHeaderInfo.context.getString(R.string.extra_info_format).format(
card.totalDownloads,
if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString(
R.string.episodes
),
mbString
)
} catch (t: Throwable) {
// you probably formatted incorrectly
downloadHeaderInfo.text = "Error"
logError(t)
}
episodeHolder.setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(0, d))
}
}
holder.setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(0, d))
}
}
}
override fun reattachDownloadButton() {
downloadButton.dispose()
val card = localCard
if (card?.child != null) {
downloadButton.setUpButton(
card.currentBytes,
card.totalBytes,
downloadBar,
downloadImage,
extraInfo,
card.child,
movieClickCallback
)
}
}
}

View file

@ -0,0 +1,199 @@
package com.lagradost.cloudstream3.ui.download.button
import android.content.Context
import android.text.format.Formatter
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.TextView
import androidx.annotation.LayoutRes
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.VideoDownloadManager
typealias DownloadStatusTell = VideoDownloadManager.DownloadType
data class DownloadMetadata(
var id: Int,
var downloadedLength: Long,
var totalLength: Long,
var status: DownloadStatusTell? = null
) {
val progressPercentage: Long
get() = if (downloadedLength < 1024) 0 else maxOf(
0,
minOf(100, (downloadedLength * 100L) / totalLength)
)
}
abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
FrameLayout(context, attributeSet) {
var persistentId: Int? = null // used to save sessions
lateinit var progressBar: ContentLoadingProgressBar
var progressText: TextView? = null
/*val gid: String? get() = sessionIdToGid[persistentId]
// used for resuming data
var _lastRequestOverride: UriRequest? = null
var lastRequest: UriRequest?
get() = _lastRequestOverride ?: sessionIdToLastRequest[persistentId]
set(value) {
_lastRequestOverride = value
}
var files: List<AbstractClient.JsonFile> = emptyList()*/
protected var isZeroBytes: Boolean = true
fun inflate(@LayoutRes layout: Int) {
inflate(context, layout, this)
}
init {
resetViewData()
}
open fun resetViewData() {
// lastRequest = null
isZeroBytes = true
persistentId = null
}
var currentMetaData: DownloadMetadata =
DownloadMetadata(0, 0, 0, null)
fun setPersistentId(id: Int) {
persistentId = id
currentMetaData.id = id
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(context, id)?.let { savedData ->
val downloadedBytes = savedData.fileLength
val totalBytes = savedData.totalBytes
/*lastRequest = savedData.uriRequest
files = savedData.files
var totalBytes: Long = 0
var downloadedBytes: Long = 0
for (file in savedData.files) {
downloadedBytes += file.completedLength
totalBytes += file.length
}*/
setProgress(downloadedBytes, totalBytes)
// some extra padding for just in case
val status = VideoDownloadManager.downloadStatus[id]
?: if (downloadedBytes > 1024L && downloadedBytes + 1024L >= totalBytes) DownloadStatusTell.IsDone else DownloadStatusTell.IsPaused
currentMetaData.apply {
this.id = id
this.downloadedLength = downloadedBytes
this.totalLength = totalBytes
this.status = status
}
setStatus(status)
} ?: run {
resetView()
}
}
abstract fun setStatus(status: VideoDownloadManager.DownloadType?)
open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
isZeroBytes = downloadedBytes == 0L
val steps = 10000L
progressBar.max = steps.toInt()
// div by zero error and 1 byte off is ok impo
val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt()
val animation = ProgressBarAnimation(
progressBar,
progressBar.progress.toFloat(),
progress.toFloat()
).apply {
fillAfter = true
duration =
if (progress > progressBar.progress) // we don't want to animate backward changes in progress
100
else
0L
}
if (isZeroBytes) {
progressText?.isVisible = false
} else {
progressText?.apply {
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes)
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
text =
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
context?.getString(R.string.download_size_format)
?.format(currentMbString, totalMbString)
}
}
progressBar.startAnimation(animation)
}
fun downloadStatusEvent(data: Pair<Int, VideoDownloadManager.DownloadType>) {
val (id, status) = data
if (id == persistentId) {
currentMetaData.status = status
setStatus(status)
}
}
/*fun downloadDeleteEvent(data: Int) {
}*/
/*fun downloadEvent(data: Pair<Int, VideoDownloadManager.DownloadActionType>) {
val (id, action) = data
}*/
fun downloadProgressEvent(data: Triple<Int, Long, Long>) {
val (id, bytesDownloaded, bytesTotal) = data
if (id == persistentId) {
currentMetaData.downloadedLength = bytesDownloaded
currentMetaData.totalLength = bytesTotal
setProgress(bytesDownloaded, bytesTotal)
}
}
override fun onAttachedToWindow() {
VideoDownloadManager.downloadStatusEvent += ::downloadStatusEvent
//VideoDownloadManager.downloadDeleteEvent += ::downloadDeleteEvent
//VideoDownloadManager.downloadEvent += ::downloadEvent
VideoDownloadManager.downloadProgressEvent += ::downloadProgressEvent
val pid = persistentId
if (pid != null) {
// refresh in case of onDetachedFromWindow -> onAttachedToWindow while still being ???????
setPersistentId(pid)
}
super.onAttachedToWindow()
}
override fun onDetachedFromWindow() {
VideoDownloadManager.downloadStatusEvent -= ::downloadStatusEvent
//VideoDownloadManager.downloadDeleteEvent -= ::downloadDeleteEvent
//VideoDownloadManager.downloadEvent -= ::downloadEvent
VideoDownloadManager.downloadProgressEvent -= ::downloadProgressEvent
super.onDetachedFromWindow()
}
/**
* No checks required. Arg will always include a download with current id
* */
abstract fun updateViewOnDownload(metadata: DownloadMetadata)
/**
* Get a clean slate again, might be useful in recyclerview?
* */
abstract fun resetView()
}

View file

@ -0,0 +1,56 @@
package com.lagradost.cloudstream3.ui.download.button
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.core.view.isVisible
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
class DownloadButton(context: Context, attributeSet: AttributeSet) :
PieFetchButton(context, attributeSet) {
var mainText: TextView? = null
override fun onAttachedToWindow() {
super.onAttachedToWindow()
progressText = findViewById(R.id.result_movie_download_text_precentage)
mainText = findViewById(R.id.result_movie_download_text)
}
override fun setStatus(status: DownloadStatusTell?) {
super.setStatus(status)
val txt = when (status) {
DownloadStatusTell.IsPaused -> R.string.download_paused
DownloadStatusTell.IsDownloading -> R.string.downloading
DownloadStatusTell.IsDone -> R.string.downloaded
else -> R.string.download
}
mainText?.setText(txt)
}
override fun setDefaultClickListener(
card: VideoDownloadHelper.DownloadEpisodeCached,
textView: TextView?,
callback: (DownloadClickEvent) -> Unit
) {
this.setDefaultClickListener(
this.findViewById<MaterialButton>(R.id.download_movie_button),
textView,
card,
callback
)
}
@SuppressLint("SetTextI18n")
override fun updateViewOnDownload(metadata: DownloadMetadata) {
super.updateViewOnDownload(metadata)
val isVis = metadata.progressPercentage > 0
progressText?.isVisible = isVis
if (isVis)
progressText?.text = "${metadata.progressPercentage}%"
}
}

View file

@ -0,0 +1,322 @@
package com.lagradost.cloudstream3.ui.download.button
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.Log
import android.view.View
import android.view.animation.AnimationUtils
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PAUSE_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_RESUME_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
BaseFetchButton(context, attributeSet) {
private var waitingAnimation: Int = 0
private var animateWaiting: Boolean = false
private var activeOutline: Int = 0
private var nonActiveOutline: Int = 0
private var iconInit: Int = 0
private var iconError: Int = 0
private var iconComplete: Int = 0
private var iconActive: Int = 0
private var iconWaiting: Int = 0
private var iconRemoved: Int = 0
private var iconPaused: Int = 0
private var hideWhenIcon: Boolean = true
var overrideLayout: Int? = null
companion object {
val fillArray = arrayOf(
R.drawable.circular_progress_bar_clockwise,
R.drawable.circular_progress_bar_counter_clockwise,
R.drawable.circular_progress_bar_small_to_large,
R.drawable.circular_progress_bar_top_to_bottom,
)
}
private var progressBarBackground: View
private var statusView: ImageView
open fun onInflate() {}
init {
context.obtainStyledAttributes(attributeSet, R.styleable.PieFetchButton, 0, 0).apply {
try {
inflate(
overrideLayout ?: getResourceId(
R.styleable.PieFetchButton_download_layout,
R.layout.download_button_view
)
)
} catch (e: Exception) {
Log.e(
"PieFetchButton", "Error inflating PieFetchButton, " +
"check that you have declared the required aria2c attrs: aria2c_icon_scale aria2c_icon_color aria2c_outline_color aria2c_fill_color"
)
throw e
}
progressBar = findViewById(R.id.progress_downloaded)
progressBarBackground = findViewById(R.id.progress_downloaded_background)
statusView = findViewById(R.id.image_download_status)
animateWaiting = getBoolean(
R.styleable.PieFetchButton_download_animate_waiting,
true
)
hideWhenIcon = getBoolean(
R.styleable.PieFetchButton_download_hide_when_icon,
true
)
waitingAnimation = getResourceId(
R.styleable.PieFetchButton_download_waiting_animation,
R.anim.rotate_around_center_point
)
activeOutline = getResourceId(
R.styleable.PieFetchButton_download_outline_active, R.drawable.circle_shape
)
nonActiveOutline = getResourceId(
R.styleable.PieFetchButton_download_outline_non_active,
R.drawable.circle_shape_dotted
)
iconInit = getResourceId(
R.styleable.PieFetchButton_download_icon_init, R.drawable.netflix_download
)
iconError = getResourceId(
R.styleable.PieFetchButton_download_icon_paused, R.drawable.download_icon_error
)
iconComplete = getResourceId(
R.styleable.PieFetchButton_download_icon_complete, R.drawable.download_icon_done
)
iconPaused = getResourceId(
R.styleable.PieFetchButton_download_icon_paused, 0//R.drawable.download_icon_pause
)
iconActive = getResourceId(
R.styleable.PieFetchButton_download_icon_active, 0 //R.drawable.download_icon_load
)
iconWaiting = getResourceId(
R.styleable.PieFetchButton_download_icon_waiting, 0
)
iconRemoved = getResourceId(
R.styleable.PieFetchButton_download_icon_removed, R.drawable.netflix_download
)
val fillIndex = getInt(R.styleable.PieFetchButton_download_fill, 0)
val progressDrawable = getResourceId(
R.styleable.PieFetchButton_download_fill_override, fillArray[fillIndex]
)
progressBar.progressDrawable = ContextCompat.getDrawable(context, progressDrawable)
recycle()
}
resetView()
onInflate()
}
private var currentStatus: DownloadStatusTell? = null
/*private fun getActivity(): Activity? {
var context = context
while (context is ContextWrapper) {
if (context is Activity) {
return context
}
context = context.baseContext
}
return null
}
fun callback(event : DownloadClickEvent) {
handleDownloadClick(
getActivity(),
event
)
}*/
protected fun setDefaultClickListener(
view: View, textView: TextView?, card: VideoDownloadHelper.DownloadEpisodeCached,
callback: (DownloadClickEvent) -> Unit
) {
this.progressText = textView
this.setPersistentId(card.id)
view.setOnClickListener {
if (isZeroBytes) {
callback(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, card))
//callback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
} else {
val list = arrayListOf(
Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file),
Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file),
)
currentMetaData.apply {
// DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone &&
if ((downloadedLength * 100 / totalLength) < 98) {
list.add(
if (status == VideoDownloadManager.DownloadType.IsDownloading)
Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download)
else
Pair(
DOWNLOAD_ACTION_RESUME_DOWNLOAD,
R.string.popup_resume_download
)
)
}
}
it.popupMenuNoIcons(
list
) {
callback(DownloadClickEvent(itemId, card))
//callback.invoke(DownloadClickEvent(itemId, data))
}
}
}
view.setOnLongClickListener {
callback(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, card))
//clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data))
return@setOnLongClickListener true
}
}
open fun setDefaultClickListener(
card: VideoDownloadHelper.DownloadEpisodeCached,
textView: TextView?,
callback: (DownloadClickEvent) -> Unit
) {
setDefaultClickListener(this, textView, card, callback)
}
/*open fun setDefaultClickListener(requestGetter: suspend BaseFetchButton.() -> List<UriRequest>) {
this.setOnClickListener {
when (this.currentStatus) {
null -> {
setStatus(DownloadStatusTell.IsPending)
ioThread {
val request = requestGetter.invoke(this)
if (request.size == 1) {
performDownload(request.first())
} else if (request.isNotEmpty()) {
performFailQueueDownload(request)
}
}
}
DownloadStatusTell.Paused -> {
resumeDownload()
}
DownloadStatusTell.Active -> {
pauseDownload()
}
DownloadStatusTell.Error -> {
redownload()
}
else -> {}
}
}
}*/
/** Also sets currentStatus */
override fun setStatus(status: DownloadStatusTell?) {
currentStatus = status
//progressBar.isVisible =
// status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error
//progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
progressBarBackground.startAnimation(animation)
} else {
progressBarBackground.clearAnimation()
}
val progressDrawable =
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline
progressBarBackground.background =
ContextCompat.getDrawable(context, progressDrawable)
val drawable = getDrawableFromStatus(status)
statusView.setImageDrawable(drawable)
val isDrawable = drawable != null
statusView.isVisible = isDrawable
val hide = hideWhenIcon && isDrawable
if (hide) {
progressBar.clearAnimation()
progressBarBackground.clearAnimation()
}
progressBarBackground.isGone = hide
progressBar.isGone = hide
}
override fun resetView() {
setStatus(null)
currentMetaData = DownloadMetadata(0, 0, 0, null)
isZeroBytes = true
progressBar.progress = 0
}
override fun updateViewOnDownload(metadata: DownloadMetadata) {
val newStatus = metadata.status
if (newStatus == null) {
resetView()
return
}
val isDone =
newStatus == DownloadStatusTell.IsDone || (metadata.downloadedLength > 1024 && metadata.downloadedLength + 1024 >= metadata.totalLength)
if (isDone)
setStatus(DownloadStatusTell.IsDone)
else {
setProgress(metadata.downloadedLength, metadata.totalLength)
setStatus(newStatus)
}
}
open fun getDrawableFromStatus(status: DownloadStatusTell?): Drawable? {
val drawableInt = when (status) {
DownloadStatusTell.IsPaused -> iconPaused
DownloadStatusTell.IsPending -> iconWaiting
DownloadStatusTell.IsDownloading -> iconActive
DownloadStatusTell.IsFailed -> iconError
DownloadStatusTell.IsDone -> iconComplete
DownloadStatusTell.IsStopped -> iconRemoved
null -> iconInit
}
if (drawableInt == 0) {
return null
}
return ContextCompat.getDrawable(this.context, drawableInt)
}
}

View file

@ -0,0 +1,18 @@
package com.lagradost.cloudstream3.ui.download.button
import android.view.animation.Animation
import android.view.animation.Transformation
import android.widget.ProgressBar
class ProgressBarAnimation(
private val progressBar: ProgressBar,
private val from: Float,
private val to: Float
) :
Animation() {
override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
super.applyTransformation(interpolatedTime, t)
val value = from + (to - from) * interpolatedTime
progressBar.progress = value.toInt()
}
}

View file

@ -1,22 +1,23 @@
package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.home_result_grid.view.background_card
import kotlinx.android.synthetic.main.home_result_grid_expanded.view.*
class HomeChildItemAdapter(
val cardList: MutableList<SearchResponse>,
private val overrideLayout: Int? = null,
private val nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null,
private val clickCallback: (SearchClickCallback) -> Unit,
@ -26,16 +27,28 @@ class HomeChildItemAdapter(
var hasNext: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = overrideLayout
?: if (parent.context.IsBottomLayout()) R.layout.home_result_grid_expanded else R.layout.home_result_grid
val expanded = parent.context.IsBottomLayout()
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
val root = LayoutInflater.from(parent.context).inflate(layout, parent, false)
val binding = HomeResultGridBinding.bind(root)*/
val inflater = LayoutInflater.from(parent.context)
val binding = if (expanded) HomeResultGridExpandedBinding.inflate(
inflater,
parent,
false
) else HomeResultGridBinding.inflate(inflater, parent, false)
return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false),
binding,
clickCallback,
itemCount,
nextFocusUp,
nextFocusDown,
isHorizontal
isHorizontal,
parent.isRtl()
)
}
@ -69,14 +82,15 @@ class HomeChildItemAdapter(
class CardViewHolder
constructor(
itemView: View,
val binding: ViewBinding,
private val clickCallback: (SearchClickCallback) -> Unit,
var itemCount: Int,
private val nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null,
private val isHorizontal: Boolean = false
private val isHorizontal: Boolean = false,
private val isRtl : Boolean
) :
RecyclerView.ViewHolder(itemView) {
RecyclerView.ViewHolder(binding.root) {
fun bind(card: SearchResponse, position: Int) {
@ -87,26 +101,71 @@ class HomeChildItemAdapter(
else -> null
}
(itemView.image_holder ?: itemView.background_card)?.apply {
val min = 114.toPx
val max = 180.toPx
layoutParams =
layoutParams.apply {
width = if (!isHorizontal) {
min
} else {
max
}
height = if (!isHorizontal) {
max
} else {
min
}
}
if (position == 0) { // to fix tv
if (isRtl) {
itemView.nextFocusRightId = R.id.nav_rail_view
itemView.nextFocusLeftId = -1
}
else {
itemView.nextFocusLeftId = R.id.nav_rail_view
itemView.nextFocusRightId = -1
}
} else {
itemView.nextFocusRightId = -1
itemView.nextFocusLeftId = -1
}
when (binding) {
is HomeResultGridBinding -> {
binding.backgroundCard.apply {
val min = 114.toPx
val max = 180.toPx
layoutParams =
layoutParams.apply {
width = if (!isHorizontal) {
min
} else {
max
}
height = if (!isHorizontal) {
max
} else {
min
}
}
}
}
is HomeResultGridExpandedBinding -> {
binding.backgroundCard.apply {
val min = 114.toPx
val max = 180.toPx
layoutParams =
layoutParams.apply {
width = if (!isHorizontal) {
min
} else {
max
}
height = if (!isHorizontal) {
max
} else {
min
}
}
}
if (position == 0) { // to fix tv
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
}
}
}
SearchResultBuilder.bind(
clickCallback,
card,
@ -118,9 +177,6 @@ class HomeChildItemAdapter(
)
itemView.tag = position
if (position == 0) { // to fix tv
itemView.background_card?.nextFocusLeftId = R.id.nav_rail_view
}
//val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f)
//ani.fillAfter = true
//ani.duration = 200

View file

@ -31,17 +31,23 @@ import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding
import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding
import com.lagradost.cloudstream3.databinding.TvtypesChipsBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@ -64,24 +70,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import kotlinx.android.synthetic.main.activity_main_tv.*
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.home_api_fab
import kotlinx.android.synthetic.main.fragment_home.home_change_api_loading
import kotlinx.android.synthetic.main.fragment_home.home_loading
import kotlinx.android.synthetic.main.fragment_home.home_loading_error
import kotlinx.android.synthetic.main.fragment_home.home_loading_shimmer
import kotlinx.android.synthetic.main.fragment_home.home_loading_statusbar
import kotlinx.android.synthetic.main.fragment_home.home_master_recycler
import kotlinx.android.synthetic.main.fragment_home.home_reload_connection_open_in_browser
import kotlinx.android.synthetic.main.fragment_home.home_reload_connectionerror
import kotlinx.android.synthetic.main.fragment_home.result_error_text
import kotlinx.android.synthetic.main.fragment_home_tv.*
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.home_episodes_expanded.*
import kotlinx.android.synthetic.main.tvtypes_chips.*
import kotlinx.android.synthetic.main.tvtypes_chips.view.*
import java.util.*
@ -125,22 +114,26 @@ class HomeFragment : Fragment() {
expand: HomeViewModel.ExpandableHomepageList,
deleteCallback: (() -> Unit)? = null,
expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null,
dismissCallback : (() -> Unit),
dismissCallback: (() -> Unit),
): BottomSheetDialog {
val context = this
val bottomSheetDialogBuilder = BottomSheetDialog(context)
bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded)
val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
val binding: HomeEpisodesExpandedBinding = HomeEpisodesExpandedBinding.inflate(
bottomSheetDialogBuilder.layoutInflater,
null,
false
)
bottomSheetDialogBuilder.setContentView(binding.root)
//val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
//title.findViewTreeLifecycleOwner().lifecycle.addObserver()
val item = expand.list
title.text = item.name
val recycle =
bottomSheetDialogBuilder.findViewById<AutofitRecyclerView>(R.id.home_expanded_recycler)!!
val titleHolder =
bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!!
binding.homeExpandedText.text = item.name
// val recycle =
// bottomSheetDialogBuilder.findViewById<AutofitRecyclerView>(R.id.home_expanded_recycler)!!
//val titleHolder =
// bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!!
// main {
//(bottomSheetDialogBuilder.ownerActivity as androidx.fragment.app.FragmentActivity?)?.supportFragmentManager?.fragments?.lastOrNull()?.viewLifecycleOwner?.apply {
@ -159,10 +152,10 @@ class HomeFragment : Fragment() {
// })
//}
// }
val delete = bottomSheetDialogBuilder.home_expanded_delete
delete.isGone = deleteCallback == null
//val delete = bottomSheetDialogBuilder.home_expanded_delete
binding.homeExpandedDelete.isGone = deleteCallback == null
if (deleteCallback != null) {
delete.setOnClickListener {
binding.homeExpandedDelete.setOnClickListener {
try {
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
val dialogClickListener =
@ -172,6 +165,7 @@ class HomeFragment : Fragment() {
deleteCallback.invoke()
bottomSheetDialogBuilder.dismissSafe(this)
}
DialogInterface.BUTTON_NEGATIVE -> {}
}
}
@ -191,26 +185,27 @@ class HomeFragment : Fragment() {
}
}
}
titleHolder.setOnClickListener {
binding.homeExpandedDragDown.setOnClickListener {
bottomSheetDialogBuilder.dismissSafe(this)
}
// Span settings
recycle.spanCount = currentSpan
binding.homeExpandedRecycler.spanCount = currentSpan
recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback ->
handleSearchClickCallback(this, callback)
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later
//bottomSheetDialogBuilder.dismissSafe(this)
binding.homeExpandedRecycler.adapter =
SearchAdapter(item.list.toMutableList(), binding.homeExpandedRecycler) { callback ->
handleSearchClickCallback(callback)
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later
//bottomSheetDialogBuilder.dismissSafe(this)
}
}.apply {
hasNext = expand.hasNext
}
}.apply {
hasNext = expand.hasNext
}
recycle.addOnScrollListener(object : RecyclerView.OnScrollListener() {
binding.homeExpandedRecycler.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
var expandCount = 0
val name = expand.list.name
@ -238,7 +233,7 @@ class HomeFragment : Fragment() {
})
val spanListener = { span: Int ->
recycle.spanCount = span
binding.homeExpandedRecycler.spanCount = span
//(recycle.adapter as SearchAdapter).notifyDataSetChanged()
}
@ -280,19 +275,19 @@ class HomeFragment : Fragment() {
)
}
private fun getPairList(header: ChipGroup) = getPairList(
header.home_select_anime,
header.home_select_cartoons,
header.home_select_tv_series,
header.home_select_documentaries,
header.home_select_movies,
header.home_select_asian,
header.home_select_livestreams,
header.home_select_nsfw,
header.home_select_others
private fun getPairList(header: TvtypesChipsBinding) = getPairList(
header.homeSelectAnime,
header.homeSelectCartoons,
header.homeSelectTvSeries,
header.homeSelectDocumentaries,
header.homeSelectMovies,
header.homeSelectAsian,
header.homeSelectLivestreams,
header.homeSelectNsfw,
header.homeSelectOthers
)
fun validateChips(header: ChipGroup?, validTypes: List<TvType>) {
fun validateChips(header: TvtypesChipsBinding?, validTypes: List<TvType>) {
if (header == null) return
val pairList = getPairList(header)
for ((button, types) in pairList) {
@ -301,7 +296,7 @@ class HomeFragment : Fragment() {
}
}
fun updateChips(header: ChipGroup?, selectedTypes: List<TvType>) {
fun updateChips(header: TvtypesChipsBinding?, selectedTypes: List<TvType>) {
if (header == null) return
val pairList = getPairList(header)
for ((button, types) in pairList) {
@ -311,7 +306,7 @@ class HomeFragment : Fragment() {
}
fun bindChips(
header: ChipGroup?,
header: TvtypesChipsBinding?,
selectedTypes: List<TvType>,
validTypes: List<TvType>,
callback: (List<TvType>) -> Unit
@ -344,7 +339,13 @@ class HomeFragment : Fragment() {
BottomSheetDialog(this)
builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED
builder.setContentView(R.layout.home_select_mainpage)
val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate(
builder.layoutInflater,
null,
false
)
builder.setContentView(binding.root)
builder.show()
builder.let { dialog ->
val isMultiLang = getApiProviderLangSettings().let { set ->
@ -360,14 +361,11 @@ class HomeFragment : Fragment() {
?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries)
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
cancelBtt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe()
}
applyBtt?.setOnClickListener {
binding.applyBtt.setOnClickListener {
if (currentApiName != selectedApiName) {
currentApiName?.let(callback)
}
@ -408,7 +406,7 @@ class HomeFragment : Fragment() {
}
bindChips(
dialog.home_select_group,
binding.tvtypesChipsScroll.tvtypesChips,
preSelectedTypes,
validAPIs.flatMap { it.supportedTypes }.distinct()
) { list ->
@ -423,6 +421,9 @@ class HomeFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels()
var binding: FragmentHomeBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -430,14 +431,25 @@ class HomeFragment : Fragment() {
): View? {
//homeViewModel =
// ViewModelProvider(this).get(HomeViewModel::class.java)
bottomSheetDialog?.ownShow()
val layout =
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home
return inflater.inflate(layout, container, false)
val root = inflater.inflate(layout, container, false)
binding = try {
FragmentHomeBinding.bind(root)
} catch (t: Throwable) {
showToast(txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG)
logError(t)
null
}
return root
}
override fun onDestroyView() {
bottomSheetDialog?.ownHide()
binding = null
super.onDestroyView()
}
@ -450,7 +462,7 @@ class HomeFragment : Fragment() {
private val apiChangeClickListener = View.OnClickListener { view ->
view.context.selectHomepage(currentApiName) { api ->
homeViewModel.loadAndCancel(api)
homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true)
}
/*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf()
@ -467,196 +479,149 @@ class HomeFragment : Fragment() {
fixGrid()
}
fun bookmarksUpdated(_data : Boolean) {
reloadStored()
}
override fun onResume() {
super.onResume()
reloadStored()
bookmarksUpdatedEvent += ::bookmarksUpdated
afterPluginsLoadedEvent += ::afterPluginsLoaded
mainPluginsLoadedEvent += ::afterMainPluginsLoaded
}
override fun onStop() {
bookmarksUpdatedEvent -= ::bookmarksUpdated
afterPluginsLoadedEvent -= ::afterPluginsLoaded
mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
super.onStop()
}
private fun reloadStored() {
homeViewModel.loadResumeWatching()
val list = EnumSet.noneOf(WatchType::class.java)
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
list.addAll(it)
}
homeViewModel.loadStoredData(list)
}
private fun afterMainPluginsLoaded(unused: Boolean = false) {
loadHomePage(false)
}
private fun afterPluginsLoaded(forceReload: Boolean) {
loadHomePage(forceReload)
}
private fun loadHomePage(forceReload: Boolean) {
val apiName = context?.getKey<String>(USER_SELECTED_HOMEPAGE_API)
if (homeViewModel.apiName.value != apiName || apiName == null || forceReload) {
//println("Caught home: " + homeViewModel.apiName.value + " at " + apiName)
homeViewModel.loadAndCancel(apiName, forceReload)
}
}
private fun homeHandleSearch(callback: SearchClickCallback) {
if (callback.action == SEARCH_ACTION_FOCUSED) {
//focusCallback(callback.card)
} else {
handleSearchClickCallback(activity, callback)
}
}
private var currentApiName: String? = null
private var toggleRandomButton = false
private var bottomSheetDialog: BottomSheetDialog? = null
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fixGrid()
home_change_api_loading?.setOnClickListener(apiChangeClickListener)
home_api_fab?.setOnClickListener(apiChangeClickListener)
home_random?.setOnClickListener {
if (listHomepageItems.isNotEmpty()) {
activity.loadSearchResult(listHomepageItems.random())
binding?.apply {
homeChangeApiLoading.setOnClickListener(apiChangeClickListener)
//homeChangeApiLoading.setOnClickListener(apiChangeClickListener)
homeApiFab.setOnClickListener(apiChangeClickListener)
homeRandom.setOnClickListener {
if (listHomepageItems.isNotEmpty()) {
activity.loadSearchResult(listHomepageItems.random())
}
}
homeMasterRecycler.adapter =
HomeParentItemAdapterPreview(
mutableListOf(),
homeViewModel
)
fixPaddingStatusbar(homeLoadingStatusbar)
if (isTvSettings()) {
homeApiFab.isVisible = false
if (isTrueTvSettings()) {
homeChangeApiLoading.isVisible = true
homeChangeApiLoading.isFocusable = true
homeChangeApiLoading.isFocusableInTouchMode = true
}
// home_bookmark_select?.isFocusable = true
// home_bookmark_select?.isFocusableInTouchMode = true
} else {
homeApiFab.isVisible = true
homeChangeApiLoading.isVisible = false
}
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { //check for scroll down
homeApiFab.shrink() // hide
homeRandom.shrink()
} else if (dy < -5) {
if (!isTvSettings()) {
homeApiFab.extend() // show
homeRandom.extend()
}
}
super.onScrolled(recyclerView, dx, dy)
}
})
}
//Load value for toggling Random button. Hide at startup
context?.let {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
toggleRandomButton =
settingsManager.getBoolean(getString(R.string.random_button_key), false)
home_random?.visibility = View.GONE
}
observe(homeViewModel.preview) { preview ->
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData(
preview
)
settingsManager.getBoolean(
getString(R.string.random_button_key),
false
) && !isTvSettings()
binding?.homeRandom?.visibility = View.GONE
}
observe(homeViewModel.apiName) { apiName ->
currentApiName = apiName
home_api_fab?.text = apiName
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName(
apiName
)
binding?.homeApiFab?.text = apiName
}
observe(homeViewModel.page) { data ->
when (data) {
is Resource.Success -> {
home_loading_shimmer?.stopShimmer()
binding?.apply {
when (data) {
is Resource.Success -> {
homeLoadingShimmer.stopShimmer()
val d = data.value
val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear()
val d = data.value
val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear()
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(
d.values.toMutableList(),
home_master_recycler
)
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(
d.values.toMutableList(),
homeMasterRecycler
)
home_loading?.isVisible = false
home_loading_error?.isVisible = false
home_master_recycler?.isVisible = true
//home_loaded?.isVisible = true
if (toggleRandomButton) {
//Flatten list
d.values.forEach { dlist ->
mutableListOfResponse.addAll(dlist.list.list)
homeLoading.isVisible = false
homeLoadingError.isVisible = false
homeMasterRecycler.isVisible = true
//home_loaded?.isVisible = true
if (toggleRandomButton) {
//Flatten list
d.values.forEach { dlist ->
mutableListOfResponse.addAll(dlist.list.list)
}
listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url })
homeRandom.isVisible = listHomepageItems.isNotEmpty()
} else {
homeRandom.isGone = true
}
listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url })
home_random?.isVisible = listHomepageItems.isNotEmpty()
} else {
home_random?.isGone = true
}
}
is Resource.Failure -> {
home_loading_shimmer?.stopShimmer()
result_error_text.text = data.errorString
is Resource.Failure -> {
homeLoadingShimmer.stopShimmer()
resultErrorText.text = data.errorString
homeReloadConnectionerror.setOnClickListener(apiChangeClickListener)
homeReloadConnectionOpenInBrowser.setOnClickListener { view ->
val validAPIs = apis//.filter { api -> api.hasMainPage }
home_reload_connectionerror.setOnClickListener(apiChangeClickListener)
home_reload_connection_open_in_browser.setOnClickListener { view ->
val validAPIs = apis//.filter { api -> api.hasMainPage }
view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api ->
Pair(
index,
api.name
)
}) {
try {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(validAPIs[itemId].mainUrl)
startActivity(i)
} catch (e: Exception) {
logError(e)
view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api ->
Pair(
index,
api.name
)
}) {
try {
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(validAPIs[itemId].mainUrl)
startActivity(i)
} catch (e: Exception) {
logError(e)
}
}
}
homeLoading.isVisible = false
homeLoadingError.isVisible = true
homeMasterRecycler.isVisible = false
//home_loaded?.isVisible = false
}
home_loading?.isVisible = false
home_loading_error?.isVisible = true
home_master_recycler?.isVisible = false
//home_loaded?.isVisible = false
}
is Resource.Loading -> {
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf())
home_loading_shimmer?.startShimmer()
home_loading?.isVisible = true
home_loading_error?.isVisible = false
home_master_recycler?.isVisible = false
//home_loaded?.isVisible = false
}
}
}
observe(homeViewModel.availableWatchStatusTypes) { availableWatchStatusTypes ->
context?.setKey(
HOME_BOOKMARK_VALUE_LIST,
availableWatchStatusTypes.first.map { it.internalId }.toIntArray()
)
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes(
availableWatchStatusTypes
)
}
observe(homeViewModel.bookmarks) { data ->
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData(
data
)
}
observe(homeViewModel.resumeWatching) { resumeWatching ->
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData(
resumeWatching
)
if (isTrueTvSettings()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ioSafe {
activity?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
is Resource.Loading -> {
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf())
homeLoadingShimmer.startShimmer()
homeLoading.isVisible = true
homeLoadingError.isVisible = false
homeMasterRecycler.isVisible = false
//home_loaded?.isVisible = false
}
}
}
@ -665,72 +630,35 @@ class HomeFragment : Fragment() {
//context?.fixPaddingStatusbarView(home_statusbar)
//context?.fixPaddingStatusbar(home_padding)
context?.fixPaddingStatusbar(home_loading_statusbar)
home_master_recycler?.adapter =
HomeParentItemAdapterPreview(mutableListOf(), { callback ->
homeHandleSearch(callback)
}, { item ->
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = {
homeViewModel.expandAndReturn(it)
}, dismissCallback = {
bottomSheetDialog = null
})
}, { name ->
homeViewModel.expand(name)
}, { load ->
activity?.loadResult(load.response.url, load.response.apiName, load.action)
}, {
homeViewModel.loadMoreHomeScrollResponses()
}, {
apiChangeClickListener.onClick(it)
}, reloadStored = {
reloadStored()
}, loadStoredData = {
homeViewModel.loadStoredData(it)
}, { (isQuickSearch, text) ->
if (!isQuickSearch) {
QuickSearchFragment.pushSearch(
activity,
text,
currentApiName?.let { arrayOf(it) })
}
})
reloadStored()
loadHomePage(false)
home_master_recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) { //check for scroll down
home_api_fab?.shrink() // hide
home_random?.shrink()
} else if (dy < -5) {
if (!isTvSettings()) {
home_api_fab?.extend() // show
home_random?.extend()
}
}
super.onScrolled(recyclerView, dx, dy)
observeNullable(homeViewModel.popup) { item ->
if (item == null) {
bottomSheetDialog?.dismissSafe()
bottomSheetDialog = null
return@observeNullable
}
})
// don't recreate
if (bottomSheetDialog != null) {
return@observeNullable
}
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = {
homeViewModel.expandAndReturn(it)
}, dismissCallback = {
homeViewModel.popup(null)
bottomSheetDialog = null
})
}
homeViewModel.reloadStored()
homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false)
//loadHomePage(false)
// nice profile pic on homepage
//home_profile_picture_holder?.isVisible = false
// just in case
if (isTvSettings()) {
home_api_fab?.isVisible = false
if (isTrueTvSettings()) {
home_change_api_loading?.isVisible = true
home_change_api_loading?.isFocusable = true
home_change_api_loading?.isFocusableInTouchMode = true
}
// home_bookmark_select?.isFocusable = true
// home_bookmark_select?.isFocusableInTouchMode = true
} else {
home_api_fab?.isVisible = true
home_change_api_loading?.isVisible = false
}
//TODO READD THIS
/*for (syncApi in OAuth2Apis) {
val login = syncApi.loginInfo()

View file

@ -3,50 +3,19 @@ package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.result.LinearListLayout
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
import kotlinx.android.synthetic.main.activity_main_tv.*
import kotlinx.android.synthetic.main.activity_main_tv.view.*
import kotlinx.android.synthetic.main.fragment_home.*
import kotlinx.android.synthetic.main.fragment_home.view.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager
import kotlinx.android.synthetic.main.homepage_parent.view.*
class LoadClickCallback(
val action: Int = 0,
@ -57,17 +26,23 @@ class LoadClickCallback(
open class ParentItemAdapter(
private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
//private val viewModel: HomeViewModel,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val root = LayoutInflater.from(parent.context).inflate(
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
parent,
false
)
val binding = HomepageParentBinding.bind(root)
return ParentViewHolder(
LayoutInflater.from(parent.context).inflate(
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
parent,
false
),
binding,
clickCallback,
moreInfoClickCallback,
expandCallback
@ -178,14 +153,15 @@ open class ParentItemAdapter(
class ParentViewHolder
constructor(
itemView: View,
val binding: HomepageParentBinding,
// val viewModel: HomeViewModel,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null,
) :
RecyclerView.ViewHolder(itemView) {
val title: TextView = itemView.home_child_more_info
private val recyclerView: RecyclerView = itemView.home_child_recyclerview
RecyclerView.ViewHolder(binding.root) {
val title: TextView = binding.homeChildMoreInfo
private val recyclerView: RecyclerView = binding.homeChildRecyclerview
fun update(expand: HomeViewModel.ExpandableHomepageList) {
val info = expand.list

View file

@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.home
import android.content.res.Configuration
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.core.view.isGone
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.fragment_home_head_tv.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
import kotlinx.android.synthetic.main.home_scroll_view.view.*
class HomeScrollAdapter(
@LayoutRes val layout: Int = R.layout.home_scroll_view,
private val forceHorizontalPosters: Boolean? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: MutableList<LoadResponse> = mutableListOf()
var hasMoreItems: Boolean = false
@ -45,9 +39,16 @@ class HomeScrollAdapter(
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val binding = if (isTvSettings()) {
HomeScrollViewTvBinding.inflate(inflater, parent, false)
} else {
HomeScrollViewBinding.inflate(inflater, parent, false)
}
return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false),
forceHorizontalPosters
binding,
//forceHorizontalPosters
)
}
@ -61,22 +62,32 @@ class HomeScrollAdapter(
class CardViewHolder
constructor(
itemView: View,
private val forceHorizontalPosters: Boolean? = null
val binding: ViewBinding,
//private val forceHorizontalPosters: Boolean? = null
) :
RecyclerView.ViewHolder(itemView) {
RecyclerView.ViewHolder(binding.root) {
fun bind(card: LoadResponse) {
card.apply {
val isHorizontal =
(forceHorizontalPosters == true) || ((forceHorizontalPosters != false) && itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
val isHorizontal =
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val posterUrl = if (isHorizontal) backgroundPosterUrl ?: posterUrl else posterUrl
?: backgroundPosterUrl
itemView.home_scroll_preview_tags?.text = tags?.joinToString("") ?: ""
itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty()
itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders)
itemView.home_scroll_preview_title?.text = name
val posterUrl =
if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
?: card.backgroundPosterUrl
when (binding) {
is HomeScrollViewBinding -> {
binding.homeScrollPreview.setImage(posterUrl)
binding.homeScrollPreviewTags.apply {
text = card.tags?.joinToString("") ?: ""
isGone = card.tags.isNullOrEmpty()
}
binding.homeScrollPreviewTitle.text = card.name
}
is HomeScrollViewTvBinding -> {
binding.homeScrollPreview.setImage(posterUrl)
}
}
}
}

View file

@ -1,10 +1,10 @@
package com.lagradost.cloudstream3.ui.home
import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
@ -13,11 +13,30 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.debugWarning
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
@ -32,7 +51,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import java.util.*
import java.util.EnumSet
import kotlin.collections.set
class HomeViewModel : ViewModel() {
@ -72,7 +91,7 @@ class HomeViewModel : ViewModel() {
}
}
private var repo: APIRepository? = null
var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>()
val apiName: LiveData<String> = _apiName
@ -83,7 +102,7 @@ class HomeViewModel : ViewModel() {
private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository {
return APIRepository(apis.first { it.hasMainPage })
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }})
}
private val _availableWatchStatusTypes =
@ -101,8 +120,14 @@ class HomeViewModel : ViewModel() {
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
fun loadResumeWatching() = viewModelScope.launchSafe {
private fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatchingResult = getResumeWatching()
if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ioSafe {
// this WILL crash on non tvs, so keep this inside a try catch
activity?.addProgramsToContinueWatching(resumeWatchingResult)
}
}
resumeWatchingResult?.let {
_resumeWatching.postValue(it)
}
@ -128,6 +153,10 @@ class HomeViewModel : ViewModel() {
currentWatchTypes.remove(WatchType.NONE)
if (currentWatchTypes.size <= 0) {
setKey(
HOME_BOOKMARK_VALUE_LIST,
intArrayOf()
)
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
_bookmarks.postValue(Pair(false, ArrayList()))
return@launchSafe
@ -135,7 +164,10 @@ class HomeViewModel : ViewModel() {
val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first())
//if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first())
setKey(
HOME_BOOKMARK_VALUE_LIST,
watchPrefNotNull.map { it.internalId }.toIntArray()
)
_availableWatchStatusTypes.postValue(
Pair(
watchPrefNotNull,
@ -152,8 +184,10 @@ class HomeViewModel : ViewModel() {
}
private var onGoingLoad: Job? = null
private fun loadAndCancel(api: MainAPI?) {
private var isCurrentlyLoadingName : String? = null
private fun loadAndCancel(api: MainAPI) {
onGoingLoad?.cancel()
isCurrentlyLoadingName = api.name
onGoingLoad = load(api)
}
@ -255,12 +289,12 @@ class HomeViewModel : ViewModel() {
}
}
private fun load(api: MainAPI?) = ioSafe {
repo = if (api != null) {
private fun load(api: MainAPI) : Job = ioSafe {
repo = //if (api != null) {
APIRepository(api)
} else {
autoloadRepo()
}
//} else {
// autoloadRepo()
//}
_apiName.postValue(repo?.name)
_randomItems.postValue(listOf())
@ -274,6 +308,7 @@ class HomeViewModel : ViewModel() {
_page.postValue(Resource.Loading())
_preview.postValue(Resource.Loading())
// cancel the current preview expand as that is no longer relevant
addJob?.cancel()
when (val data = repo?.getMainPage(1, null)) {
@ -337,41 +372,126 @@ class HomeViewModel : ViewModel() {
logError(e)
}
}
is Resource.Failure -> {
_page.postValue(data!!)
_preview.postValue(data!!)
}
else -> Unit
}
isCurrentlyLoadingName = null
}
fun click(callback: SearchClickCallback) {
if (callback.action == SEARCH_ACTION_FOCUSED) {
//focusCallback(callback.card)
} else {
SearchHelper.handleSearchClickCallback(callback)
}
}
fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) =
viewModelScope.launchSafe {
private val _popup = MutableLiveData<ExpandableHomepageList?>(null)
val popup: LiveData<ExpandableHomepageList?> = _popup
fun popup(list: ExpandableHomepageList?) {
_popup.postValue(list)
}
private fun bookmarksUpdated(unused: Boolean) {
reloadStored()
}
private fun afterPluginsLoaded(forceReload: Boolean) {
loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload)
}
private fun afterMainPluginsLoaded(unused: Boolean = false) {
loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false)
}
init {
MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated
MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded
MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded
}
override fun onCleared() {
MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated
MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded
MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
super.onCleared()
}
fun queryTextSubmit(query: String) {
QuickSearchFragment.pushSearch(
query,
repo?.name?.let { arrayOf(it) })
}
fun queryTextChange(newText: String) {
// do nothing
}
fun reloadStored() {
loadResumeWatching()
val list = EnumSet.noneOf(WatchType::class.java)
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
list.addAll(it)
}
loadStoredData(list)
}
fun click(load: LoadClickCallback) {
loadResult(load.response.url, load.response.apiName, load.action)
}
// only save the key if it is from UI, as we don't want internal functions changing the setting
fun loadAndCancel(
preferredApiName: String?,
forceReload: Boolean = true,
fromUI: Boolean = false
) =
ioSafe {
// Since plugins are loaded in stages this function can get called multiple times.
// The issue with this is that the homepage may be fetched multiple times while the first request is loading
val api = getApiFromNameNull(preferredApiName)
if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) {
return@launchSafe
// api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true
val currentPage = page.value
// if we don't need to reload and we have a valid homepage or currently loading the same thing then return
val currentLoading = isCurrentlyLoadingName
if (!forceReload && (currentPage is Resource.Success && currentPage.value.isNotEmpty() || (currentLoading != null && currentLoading == preferredApiName))) {
return@ioSafe
}
if (preferredApiName == noneApi.name) {
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
// just set to random
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
loadAndCancel(noneApi)
} else if (preferredApiName == randomApi.name) {
// randomize the api, if none exist like if not loaded or not installed
// then use nothing
val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) {
// Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded
loadAndCancel(noneApi)
} else {
val apiRandom = validAPIs.random()
loadAndCancel(apiRandom)
setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
}
// If the plugin isn't loaded yet. (Does not set the key)
} else if (api == null) {
loadAndCancel(noneApi)
// API is not found aka not loaded or removed, post the loading
// progress if waiting for plugins, otherwise nothing
if(PluginManager.loadedLocalPlugins) {
loadAndCancel(noneApi)
} else {
_page.postValue(Resource.Loading())
}
} else {
setKey(USER_SELECTED_HOMEPAGE_API, api.name)
// if the api is found, then set it to it and save key
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name)
loadAndCancel(api)
}
}

View file

@ -6,7 +6,6 @@ import android.content.res.Configuration
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -14,6 +13,7 @@ import android.view.animation.AlphaAnimation
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder
@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.observe
@ -37,7 +38,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.fragment_library.*
import kotlin.math.abs
const val LIBRARY_FOLDER = "library_folder"
@ -73,14 +73,25 @@ class LibraryFragment : Fragment() {
private val libraryViewModel: LibraryViewModel by activityViewModels()
var binding: FragmentLibraryBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_library, container, false)
): View {
val localBinding = FragmentLibraryBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.fragment_library, container, false)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onSaveInstanceState(outState: Bundle) {
viewpager?.currentItem?.let { currentItem ->
binding?.viewpager?.currentItem?.let { currentItem ->
outState.putInt(VIEWPAGER_ITEM_KEY, currentItem)
}
super.onSaveInstanceState(outState)
@ -88,9 +99,9 @@ class LibraryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(search_status_bar_padding)
fixPaddingStatusbar(binding?.searchStatusBarPadding)
sort_fab?.setOnClickListener {
binding?.sortFab?.setOnClickListener {
val methods = libraryViewModel.sortingMethods.map {
txt(it.stringRes).asString(view.context)
}
@ -106,7 +117,7 @@ class LibraryFragment : Fragment() {
})
}
main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
libraryViewModel.sort(ListSorting.Query, query)
return true
@ -129,7 +140,7 @@ class LibraryFragment : Fragment() {
libraryViewModel.reloadPages(false)
list_selector?.setOnClickListener {
binding?.listSelector?.setOnClickListener {
val items = libraryViewModel.availableApiNames
val currentItem = libraryViewModel.currentApiName.value
@ -152,12 +163,14 @@ class LibraryFragment : Fragment() {
syncId: SyncIdName,
apiName: String? = null,
) {
val availableProviders = allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList())
val availableProviders = synchronized(allProviders) {
allProviders.filter {
it.supportedSyncNames.contains(syncId)
}.map { it.name } +
// Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList())
}
val baseOptions = listOf(
LibraryOpenerType.Default,
LibraryOpenerType.None,
@ -209,20 +222,22 @@ class LibraryFragment : Fragment() {
}
}
provider_selector?.setOnClickListener {
binding?.providerSelector?.setOnClickListener {
val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener
activity?.showPluginSelectionDialog(syncName.name, syncName)
}
viewpager?.setPageTransformer(LibraryScrollTransformer())
viewpager?.adapter =
viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean ->
if (isScrollingDown) {
sort_fab?.shrink()
} else {
sort_fab?.extend()
}
}) callback@{ searchClickCallback ->
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
binding?.viewpager?.adapter =
binding?.viewpager?.adapter ?: ViewpagerAdapter(
mutableListOf(),
{ isScrollingDown: Boolean ->
if (isScrollingDown) {
binding?.sortFab?.shrink()
} else {
binding?.sortFab?.extend()
}
}) callback@{ searchClickCallback ->
// To prevent future accidents
debugAssert({
searchClickCallback.card !is SyncAPI.LibraryItem
@ -267,6 +282,7 @@ class LibraryFragment : Fragment() {
)
}
}
LibraryOpenerType.None -> {}
LibraryOpenerType.Provider ->
savedSelection.providerData?.apiName?.let { apiName ->
@ -275,8 +291,10 @@ class LibraryFragment : Fragment() {
apiName,
)
}
LibraryOpenerType.Browser ->
openBrowser(searchClickCallback.card.url)
LibraryOpenerType.Search -> {
QuickSearchFragment.pushSearch(
activity,
@ -288,22 +306,28 @@ class LibraryFragment : Fragment() {
}
}
viewpager?.offscreenPageLimit = 2
viewpager?.reduceDragSensitivity()
binding?.apply {
viewpager.offscreenPageLimit = 2
viewpager.reduceDragSensitivity()
}
val startLoading = Runnable {
gridview?.numColumns = context?.getSpanCount() ?: 3
gridview?.adapter =
context?.let { LoadingPosterAdapter(it, 6 * 3) }
library_loading_overlay?.isVisible = true
library_loading_shimmer?.startShimmer()
empty_list_textview?.isVisible = false
binding?.apply {
gridview.numColumns = context?.getSpanCount() ?: 3
gridview.adapter =
context?.let { LoadingPosterAdapter(it, 6 * 3) }
libraryLoadingOverlay.isVisible = true
libraryLoadingShimmer.startShimmer()
emptyListTextview.isVisible = false
}
}
val stopLoading = Runnable {
gridview?.adapter = null
library_loading_overlay?.isVisible = false
library_loading_shimmer?.stopShimmer()
binding?.apply {
gridview.adapter = null
libraryLoadingOverlay.isVisible = false
libraryLoadingShimmer.stopShimmer()
}
}
val handler = Handler(Looper.getMainLooper())
@ -314,65 +338,75 @@ class LibraryFragment : Fragment() {
handler.removeCallbacks(startLoading)
val pages = resource.value
val showNotice = pages.all { it.items.isEmpty() }
empty_list_textview?.isVisible = showNotice
if (showNotice) {
if (libraryViewModel.availableApiNames.size > 1) {
empty_list_textview?.setText(R.string.empty_library_logged_in_message)
} else {
empty_list_textview?.setText(R.string.empty_library_no_accounts_message)
binding?.apply {
emptyListTextview.isVisible = showNotice
if (showNotice) {
if (libraryViewModel.availableApiNames.size > 1) {
emptyListTextview.setText(R.string.empty_library_logged_in_message)
} else {
emptyListTextview.setText(R.string.empty_library_no_accounts_message)
}
}
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
// Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged(
0,
viewpager.adapter?.itemCount ?: 0
)
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect:
// loading -> show old viewpager -> black screen -> show new viewpager
handler.postDelayed(stopLoading, 300)
savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos ->
if (currentPos < 0) return@let
viewpager.setCurrentItem(currentPos, false)
// Using remove() sets the key to 0 instead of removing it
savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1)
}
// Since the animation to scroll multiple items is so much its better to just hide
// the viewpager a bit while the fastest animation is running
fun hideViewpager(distance: Int) {
if (distance < 3) return
val hideAnimation = AlphaAnimation(1f, 0f).apply {
duration = distance * 50L
fillAfter = true
}
val showAnimation = AlphaAnimation(0f, 1f).apply {
duration = distance * 50L
startOffset = distance * 100L
fillAfter = true
}
viewpager.startAnimation(hideAnimation)
viewpager.startAnimation(showAnimation)
}
TabLayoutMediator(
libraryTabLayout,
viewpager,
) { tab, position ->
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
tab.view.setOnClickListener {
val currentItem =
binding?.viewpager?.currentItem ?: return@setOnClickListener
val distance = abs(position - currentItem)
hideViewpager(distance)
}
}.attach()
}
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
// Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0)
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
// Without this there would be a flashing effect:
// loading -> show old viewpager -> black screen -> show new viewpager
handler.postDelayed(stopLoading, 300)
savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos ->
if (currentPos < 0) return@let
viewpager?.setCurrentItem(currentPos, false)
// Using remove() sets the key to 0 instead of removing it
savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1)
}
// Since the animation to scroll multiple items is so much its better to just hide
// the viewpager a bit while the fastest animation is running
fun hideViewpager(distance: Int) {
if (distance < 3) return
val hideAnimation = AlphaAnimation(1f, 0f).apply {
duration = distance * 50L
fillAfter = true
}
val showAnimation = AlphaAnimation(0f, 1f).apply {
duration = distance * 50L
startOffset = distance * 100L
fillAfter = true
}
viewpager?.startAnimation(hideAnimation)
viewpager?.startAnimation(showAnimation)
}
TabLayoutMediator(
library_tab_layout,
viewpager,
) { tab, position ->
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
tab.view.setOnClickListener {
val currentItem = viewpager?.currentItem ?: return@setOnClickListener
val distance = abs(position - currentItem)
hideViewpager(distance)
}
}.attach()
}
is Resource.Loading -> {
// Only start loading after 200ms to prevent loading cached lists
handler.postDelayed(startLoading, 200)
}
is Resource.Failure -> {
stopLoading.run()
// No user indication it failed :(
@ -383,7 +417,7 @@ class LibraryFragment : Fragment() {
}
override fun onConfigurationChanged(newConfig: Configuration) {
(viewpager.adapter as? ViewpagerAdapter)?.rebind()
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig)
}
}

View file

@ -2,13 +2,13 @@ package com.lagradost.cloudstream3.ui.library
import android.view.View
import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
import com.lagradost.cloudstream3.R
import kotlin.math.roundToInt
class LibraryScrollTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
val padding = (-position * page.width).roundToInt()
page.page_recyclerview.setPadding(
page.findViewById<View>(R.id.page_recyclerview).setPadding(
padding, 0,
-padding, 0
)

View file

@ -11,7 +11,6 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.delay
enum class ListSorting(@StringRes val stringRes: Int) {
Query(R.string.none),

View file

@ -5,15 +5,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.ListPopupWindow.MATCH_PARENT
import android.widget.RelativeLayout
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.loading_poster_dynamic.view.*
import kotlin.math.roundToInt
import kotlin.math.sqrt
class LoadingPosterAdapter(context: Context, private val itemCount: Int) :
BaseAdapter() {

View file

@ -3,23 +3,21 @@ package com.lagradost.cloudstream3.ui.library
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.search_result_grid_expanded.view.*
import kotlin.math.roundToInt
@ -32,8 +30,11 @@ class PageAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return LibraryItemViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.search_result_grid_expanded, parent, false)
SearchResultGridExpandedBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
@ -57,8 +58,8 @@ class PageAdapter(
}
}
inner class LibraryItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
inner class LibraryItemViewHolder(val binding: SearchResultGridExpandedBinding) :
RecyclerView.ViewHolder(binding.root) {
private val compactView = false//itemView.context.getGridIsCompact()
private val coverHeight: Int =
@ -85,11 +86,12 @@ class PageAdapter(
val fg =
getDifferentColor(bg)//palette.getVibrantColor(ContextCompat.getColor(ctx,R.color.ratingColor))
itemView.text_rating.apply {
binding.textRating.apply {
setTextColor(ColorStateList.valueOf(fg))
}
itemView.text_rating_holder?.backgroundTintList = ColorStateList.valueOf(bg)
itemView.watchProgress?.apply {
binding.textRating.compoundDrawables.getOrNull(0)?.setTint(fg)
binding.textRating.backgroundTintList = ColorStateList.valueOf(bg)
binding.watchProgress.apply {
progressTintList = ColorStateList.valueOf(fg)
progressBackgroundTintList = ColorStateList.valueOf(bg)
}
@ -99,7 +101,7 @@ class PageAdapter(
// See searchAdaptor for this, it basically fixes the height
if (!compactView) {
cardView.apply {
binding.imageView.apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
coverHeight
@ -108,23 +110,13 @@ class PageAdapter(
}
val showProgress = item.episodesCompleted != null && item.episodesTotal != null
itemView.watchProgress.isVisible = showProgress
binding.watchProgress.isVisible = showProgress
if (showProgress) {
itemView.watchProgress.max = item.episodesTotal!!
itemView.watchProgress.progress = item.episodesCompleted!!
binding.watchProgress.max = item.episodesTotal!!
binding.watchProgress.progress = item.episodesCompleted!!
}
itemView.imageText.text = item.name
val showRating = (item.personalRating ?: 0) != 0
itemView.text_rating_holder.isVisible = showRating
if (showRating) {
// We want to show 8.5 but not 8.0 hence the replace
val rating = ((item.personalRating ?: 0).toDouble() / 10).toString()
.replace(".0", "")
itemView.text_rating.text = "$rating"
}
binding.imageText.text = item.name
}
}
}

View file

@ -2,16 +2,14 @@ package com.lagradost.cloudstream3.ui.library
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.doOnAttach
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
class ViewpagerAdapter(
var pages: List<SyncAPI.Page>,
@ -20,8 +18,7 @@ class ViewpagerAdapter(
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PageViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.library_viewpager_page, parent, false)
LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
@ -34,6 +31,7 @@ class ViewpagerAdapter(
}
private val unbound = mutableSetOf<Int>()
/**
* Used to mark all pages for re-binding and forces all items to be refreshed
* Without this the pages will still use the same adapters
@ -43,44 +41,46 @@ class ViewpagerAdapter(
this.notifyItemRangeChanged(0, pages.size)
}
inner class PageViewHolder(private val itemViewTest: View) :
RecyclerView.ViewHolder(itemViewTest) {
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(page: SyncAPI.Page, rebind: Boolean) {
itemView.page_recyclerview?.spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
if (itemViewTest.page_recyclerview?.adapter == null || rebind) {
// Only add the items after it has been attached since the items rely on ItemWidth
// Which is only determined after the recyclerview is attached.
// If this fails then item height becomes 0 when there is only one item
itemViewTest.page_recyclerview?.doOnAttach {
itemViewTest.page_recyclerview?.adapter = PageAdapter(
page.items.toMutableList(),
itemViewTest.page_recyclerview,
clickCallback
)
binding.pageRecyclerview.apply {
spanCount =
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
if (adapter == null || rebind) {
// Only add the items after it has been attached since the items rely on ItemWidth
// Which is only determined after the recyclerview is attached.
// If this fails then item height becomes 0 when there is only one item
doOnAttach {
adapter = PageAdapter(
page.items.toMutableList(),
this,
clickCallback
)
}
} else {
(adapter as? PageAdapter)?.updateList(page.items)
scrollToPosition(0)
}
} else {
(itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items)
itemViewTest.page_recyclerview?.scrollToPosition(0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
val diff = scrollY - oldScrollY
if (diff == 0) return@setOnScrollChangeListener
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val diff = scrollY - oldScrollY
if (diff == 0) return@setOnScrollChangeListener
scrollCallback.invoke(diff > 0)
}
} else {
itemViewTest.page_recyclerview.onFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
scrollCallback.invoke(velocityY > 0)
return false
scrollCallback.invoke(diff > 0)
}
} else {
onFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
scrollCallback.invoke(velocityY > 0)
return false
}
}
}
}
}
}

View file

@ -11,6 +11,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
@ -41,8 +44,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.*
enum class PlayerResize(@StringRes val nameRes: Int) {
Fit(R.string.resize_fit),
@ -71,9 +72,15 @@ abstract class AbstractPlayerFragment(
var isBuffering = true
protected open var hasPipModeSupport = true
var playerPausePlayHolderHolder : FrameLayout? = null
var playerPausePlay : ImageView? = null
var playerBuffering : ProgressBar? = null
var playerView : PlayerView? = null
var piphide : FrameLayout? = null
var subtitleHolder : FrameLayout? = null
@LayoutRes
protected var layout: Int = R.layout.fragment_player
protected open var layout: Int = R.layout.fragment_player
open fun nextEpisode() {
throw NotImplementedError()
@ -132,15 +139,15 @@ abstract class AbstractPlayerFragment(
isBuffering = CSPlayerLoading.IsBuffering == isPlaying
if (isBuffering) {
player_pause_play_holder_holder?.isVisible = false
player_buffering?.isVisible = true
playerPausePlayHolderHolder?.isVisible = false
playerBuffering?.isVisible = true
} else {
player_pause_play_holder_holder?.isVisible = true
player_buffering?.isVisible = false
playerPausePlayHolderHolder?.isVisible = true
playerBuffering?.isVisible = false
if (wasPlaying != isPlaying) {
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
val drawable = player_pause_play?.drawable
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
val drawable = playerPausePlay?.drawable
var startedAnimation = false
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
@ -162,10 +169,10 @@ abstract class AbstractPlayerFragment(
// somehow the phone is wacked
if (!startedAnimation) {
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
}
} else {
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
}
}
@ -244,14 +251,12 @@ abstract class AbstractPlayerFragment(
fun showToast(message: String, gotoNext: Boolean = false) {
if (gotoNext && hasNextMirror()) {
showToast(
activity,
message,
Toast.LENGTH_SHORT
)
nextMirror()
} else {
showToast(
activity,
context?.getString(R.string.no_links_found_toast) + "\n" + message,
Toast.LENGTH_LONG
)
@ -327,9 +332,9 @@ abstract class AbstractPlayerFragment(
}
// Necessary for multiple combined videos
player_view?.setShowMultiWindowTimeBar(true)
player_view?.player = player
player_view?.performClick()
playerView?.setShowMultiWindowTimeBar(true)
playerView?.player = player
playerView?.performClick()
}
}
@ -386,9 +391,9 @@ abstract class AbstractPlayerFragment(
)
if (player is CS3IPlayer) {
subView = player_view?.findViewById(R.id.exo_subtitles)
subView = playerView?.findViewById(R.id.exo_subtitles)
subStyle = SubtitlesFragment.getCurrentSavedStyle()
player.initSubtitles(subView, subtitle_holder, subStyle)
player.initSubtitles(subView, subtitleHolder, subStyle)
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
@ -457,10 +462,10 @@ abstract class AbstractPlayerFragment(
PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
PlayerResize.Zoom -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
}
player_view?.resizeMode = type
playerView?.resizeMode = type
if (showToast)
showToast(activity, resize.nameRes, Toast.LENGTH_SHORT)
showToast(resize.nameRes, Toast.LENGTH_SHORT)
}
override fun onStop() {
@ -481,6 +486,13 @@ abstract class AbstractPlayerFragment(
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(layout, container, false)
val root = inflater.inflate(layout, container, false)
playerPausePlayHolderHolder = root.findViewById(R.id.player_pause_play_holder_holder)
playerPausePlay = root.findViewById(R.id.player_pause_play)
playerBuffering = root.findViewById(R.id.player_buffering)
playerView = root.findViewById(R.id.player_view)
piphide = root.findViewById(R.id.piphide)
subtitleHolder = root.findViewById(R.id.subtitle_holder)
return root
}
}

View file

@ -63,7 +63,15 @@ import javax.net.ssl.SSLSession
const val TAG = "CS3ExoPlayer"
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language"
/** Cache */
/** toleranceBeforeUs The maximum time that the actual position seeked to may precede the
* requested seek position, in microseconds. Must be non-negative. */
const val toleranceBeforeUs = 300_000L
/**
* toleranceAfterUs The maximum time that the actual position seeked to may exceed the requested
* seek position, in microseconds. Must be non-negative.
*/
const val toleranceAfterUs = 300_000L
class CS3IPlayer : IPlayer {
private var isPlaying = false
@ -721,7 +729,7 @@ class CS3IPlayer : IPlayer {
)
)
// Allows any seeking to be +- 0.3s to allow for faster seeking
.setSeekParameters(SeekParameters(300_000, 300_000))
.setSeekParameters(SeekParameters(toleranceBeforeUs, toleranceAfterUs))
.setLoadControl(
DefaultLoadControl.Builder()
.setTargetBufferBytes(
@ -788,7 +796,7 @@ class CS3IPlayer : IPlayer {
private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? {
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
for (lastTimeStamp in lastTimeStamps) {
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) {
if (lastTimeStamp.startMs <= position && (position + (toleranceBeforeUs / 1000L) + 1) < lastTimeStamp.endMs) {
return lastTimeStamp
}
}
@ -796,11 +804,12 @@ class CS3IPlayer : IPlayer {
}
fun updatedTime(writePosition: Long? = null) {
getCurrentTimestamp(writePosition)?.let { timestamp ->
val position = writePosition ?: exoPlayer?.currentPosition
getCurrentTimestamp(position)?.let { timestamp ->
onTimestampInvoked?.invoke(timestamp)
}
val position = writePosition ?: exoPlayer?.currentPosition
val duration = exoPlayer?.contentDuration
if (duration != null && position != null) {
playerPositionChanged?.invoke(Pair(position, duration))
@ -1086,9 +1095,9 @@ class CS3IPlayer : IPlayer {
}
override fun onRenderedFirstFrame() {
updatedTime()
super.onRenderedFirstFrame()
onRenderFirst()
updatedTime()
}
})
} catch (e: Exception) {
@ -1116,42 +1125,43 @@ class CS3IPlayer : IPlayer {
}
fun onRenderFirst() {
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
Log.i(TAG, "Rendered first frame")
val invalid = exoPlayer?.duration?.let { duration ->
// Only errors short playback when not playing downloaded files
duration < 20_000L && currentDownloadedFile == null
// Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period
// If you can get the total time that'd be better, but this is already niche.
&& exoPlayer?.currentTimeline?.periodCount == 1
&& exoPlayer?.isCurrentMediaItemLive != true
} ?: false
if (hasUsedFirstRender) { // this insures that we only call this once per player load
return
}
Log.i(TAG, "Rendered first frame")
hasUsedFirstRender = true
val invalid = exoPlayer?.duration?.let { duration ->
// Only errors short playback when not playing downloaded files
duration < 20_000L && currentDownloadedFile == null
// Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period
// If you can get the total time that'd be better, but this is already niche.
&& exoPlayer?.currentTimeline?.periodCount == 1
&& exoPlayer?.isCurrentMediaItemLive != true
} ?: false
if (invalid) {
releasePlayer(saveTime = false)
playerError?.invoke(InvalidFileException("Too short playback"))
return
}
if (invalid) {
releasePlayer(saveTime = false)
playerError?.invoke(InvalidFileException("Too short playback"))
return
}
setPreferredSubtitles(currentSubtitles)
hasUsedFirstRender = true
val format = exoPlayer?.videoFormat
val width = format?.width
val height = format?.height
if (height != null && width != null) {
playerDimensionsLoaded?.invoke(Pair(width, height))
updatedTime()
exoPlayer?.apply {
requestedListeningPercentages?.forEach { percentage ->
createMessage { _, _ ->
updatedTime()
}
.setLooper(Looper.getMainLooper())
.setPosition( /* positionMs= */contentDuration * percentage / 100)
// .setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send()
setPreferredSubtitles(currentSubtitles)
val format = exoPlayer?.videoFormat
val width = format?.width
val height = format?.height
if (height != null && width != null) {
playerDimensionsLoaded?.invoke(Pair(width, height))
updatedTime()
exoPlayer?.apply {
requestedListeningPercentages?.forEach { percentage ->
createMessage { _, _ ->
updatedTime()
}
.setLooper(Looper.getMainLooper())
.setPosition(contentDuration * percentage / 100)
// .setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send()
}
}
}

View file

@ -25,6 +25,11 @@ import com.hippo.unifile.UniFile
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
@ -33,8 +38,6 @@ import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForced
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriority
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriorityDialog
import com.lagradost.cloudstream3.ui.result.*
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
@ -49,18 +52,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.dialog_online_subtitles.*
import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt
import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt
import kotlinx.android.synthetic.main.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.android.synthetic.main.player_select_source_and_subs.*
import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings
import kotlinx.android.synthetic.main.player_select_tracks.*
import kotlinx.coroutines.Job
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.abs
class GeneratorPlayer : FullScreenPlayer() {
@ -98,12 +91,14 @@ class GeneratorPlayer : FullScreenPlayer() {
private var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none
private var binding: FragmentPlayerBinding? = null
private fun startLoading() {
player.release()
currentSelectedSubtitles = null
isActive = false
overlay_loading_skip_button?.isVisible = false
player_loading_overlay?.isVisible = true
binding?.overlayLoadingSkipButton?.isVisible = false
binding?.playerLoadingOverlay?.isVisible = true
}
private fun setSubtitles(sub: SubtitleData?): Boolean {
@ -118,7 +113,7 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onTracksInfoChanged() {
val tracks = player.getVideoTracks()
player_tracks_btt?.isVisible =
playerBinding?.playerTracksBtt?.isVisible =
tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
// Only set the preferred language if it is available.
// Otherwise it may give some users audio track init failed!
@ -158,12 +153,12 @@ class GeneratorPlayer : FullScreenPlayer() {
if (link == null) return
// manage UI
player_loading_overlay?.isVisible = false
binding?.playerLoadingOverlay?.isVisible = false
uiReset()
currentSelectedLink = link
currentMeta = viewModel.getMeta()
nextMeta = viewModel.getNextMeta()
setEpisodes(viewModel.getAllMeta() ?: emptyList())
// setEpisodes(viewModel.getAllMeta() ?: emptyList())
isActive = true
setPlayerDimen(null)
setTitle()
@ -209,7 +204,7 @@ class GeneratorPlayer : FullScreenPlayer() {
closestQuality(linkData?.quality)
)
val sourcePriority =
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name)
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.source)
// negative because we want to sort highest quality first
return qualityPriority + sourcePriority
@ -257,7 +252,9 @@ class GeneratorPlayer : FullScreenPlayer() {
val isSingleProvider = subsProviders.size == 1
val dialog = Dialog(context, R.style.AlertDialogCustomBlack)
dialog.setContentView(R.layout.dialog_online_subtitles)
val binding =
DialogOnlineSubtitlesBinding.inflate(LayoutInflater.from(context), null, false)
dialog.setContentView(binding.root)
var currentSubtitles: List<AbstractSubtitleEntities.SubtitleEntity> = emptyList()
var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null
@ -295,6 +292,7 @@ class GeneratorPlayer : FullScreenPlayer() {
imageViewEnd.setImageDrawable(drawableEnd)
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context).inflate(layout, null)
@ -318,16 +316,16 @@ class GeneratorPlayer : FullScreenPlayer() {
}
dialog.show()
dialog.cancel_btt.setOnClickListener {
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe()
}
dialog.subtitle_adapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
dialog.subtitle_adapter.adapter = arrayAdapter
binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
binding.subtitleAdapter.adapter = arrayAdapter
val adapter =
dialog.subtitle_adapter.adapter as? ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>
binding.subtitleAdapter.adapter as? ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>
dialog.subtitle_adapter.setOnItemClickListener { _, _, position, _ ->
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
}
@ -343,16 +341,16 @@ class GeneratorPlayer : FullScreenPlayer() {
val currentTempMeta = getMetaData()
// bruh idk why it is not correct
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
dialog.search_loading_bar.progressTintList = color
dialog.search_loading_bar.indeterminateTintList = color
binding.searchLoadingBar.progressTintList = color
binding.searchLoadingBar.indeterminateTintList = color
observeNullable(viewModel.currentSubtitleYear) {
// When year is changed search again
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
dialog.year_btt.text = it?.toString() ?: txt(R.string.none).asString(context)
binding.subtitlesSearch.setQuery(binding.subtitlesSearch.query, true)
binding.yearBtt.text = it?.toString() ?: txt(R.string.none).asString(context)
}
dialog.year_btt?.setOnClickListener {
binding.yearBtt.setOnClickListener {
val none = txt(R.string.none).asString(context)
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
val earliestYear = 1900
@ -380,10 +378,10 @@ class GeneratorPlayer : FullScreenPlayer() {
)
}
dialog.subtitles_search.setOnQueryTextListener(object :
binding.subtitlesSearch.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
dialog.search_loading_bar?.show()
binding.searchLoadingBar.show()
ioSafe {
val search =
AbstractSubtitleEntities.SubtitleSearch(
@ -415,7 +413,7 @@ class GeneratorPlayer : FullScreenPlayer() {
// ugly ik
activity?.runOnUiThread {
setSubtitlesList(items)
dialog.search_loading_bar?.hide()
binding.searchLoadingBar.hide()
}
}
@ -427,7 +425,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
})
dialog.search_filter.setOnClickListener { view ->
binding.searchFilter.setOnClickListener { view ->
val lang639_1 = languages.map { it.ISO_639_1 }
activity?.showDialog(languages.map { it.languageName },
lang639_1.indexOf(currentLanguageTwoLetters),
@ -436,11 +434,11 @@ class GeneratorPlayer : FullScreenPlayer() {
true,
{ }) { index ->
currentLanguageTwoLetters = lang639_1[index]
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
binding.subtitlesSearch.setQuery(binding.subtitlesSearch.query, true)
}
}
dialog.apply_btt.setOnClickListener {
binding.applyBtt.setOnClickListener {
currentSubtitle?.let { currentSubtitle ->
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
ioSafe {
@ -466,7 +464,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
dialog.show()
dialog.subtitles_search.setQuery(currentTempMeta.name, true)
binding.subtitlesSearch.setQuery(currentTempMeta.name, true)
//TODO: Set year text from currently loaded movie on Player
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
}
@ -509,7 +507,6 @@ class GeneratorPlayer : FullScreenPlayer() {
selectSourceDialog?.dismissSafe()
showToast(
activity,
String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name),
Toast.LENGTH_LONG
)
@ -558,13 +555,15 @@ class GeneratorPlayer : FullScreenPlayer() {
val currentSubtitles = sortSubs(currentSubs)
val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack)
sourceDialog.setContentView(R.layout.player_select_source_and_subs)
val binding =
PlayerSelectSourceAndSubsBinding.inflate(LayoutInflater.from(ctx), null, false)
sourceDialog.setContentView(binding.root)
selectSourceDialog = sourceDialog
sourceDialog.show()
val providerList = sourceDialog.sort_providers
val subtitleList = sourceDialog.sort_subtitles
val providerList = binding.sortProviders
val subtitleList = binding.sortSubtitles
val loadFromFileFooter: TextView =
layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView
@ -672,12 +671,12 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
sourceDialog.cancel_btt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
sourceDialog.dismissSafe(activity)
}
fun setProfileName(profile: Int) {
sourceDialog.source_settings_btt.setText(
binding.sourceSettingsBtt.setText(
QualityDataHelper.getProfileName(
profile
)
@ -685,7 +684,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
setProfileName(currentQualityProfile)
sourceDialog.profiles_click_settings.setOnClickListener {
binding.profilesClickSettings.setOnClickListener {
val activity = activity ?: return@setOnClickListener
QualityProfileDialog(
activity,
@ -699,7 +698,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}.show()
}
sourceDialog.subtitles_encoding_format?.apply {
binding.subtitlesEncodingFormat.apply {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
@ -712,7 +711,7 @@ class GeneratorPlayer : FullScreenPlayer() {
text = prefNames[if (index == -1) 0 else index]
}
sourceDialog.subtitles_click_settings?.setOnClickListener {
binding.subtitlesClickSettings.setOnClickListener {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
@ -741,7 +740,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
sourceDialog.apply_btt?.setOnClickListener {
binding.applyBtt.setOnClickListener {
var init = false
if (sourceIndex != startSource) {
init = true
@ -781,18 +780,19 @@ class GeneratorPlayer : FullScreenPlayer() {
it.height?.times(-1)
}
val currentAudioTracks = tracks.allAudioTracks
val binding: PlayerSelectTracksBinding =
PlayerSelectTracksBinding.inflate(LayoutInflater.from(ctx), null, false)
val trackDialog = Dialog(ctx, R.style.AlertDialogCustomBlack)
trackDialog.setContentView(R.layout.player_select_tracks)
trackDialog.setContentView(binding.root)
trackDialog.show()
// selectTracksDialog = tracksDialog
val videosList = trackDialog.video_tracks_list
val audioList = trackDialog.auto_tracks_list
val videosList = binding.videoTracksList
val audioList = binding.autoTracksList
trackDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1
trackDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1
binding.videoTracksHolder.isVisible = currentVideoTracks.size > 1
binding.audioTracksHolder.isVisible = currentAudioTracks.size > 1
fun dismiss() {
if (isPlaying) {
@ -857,11 +857,11 @@ class GeneratorPlayer : FullScreenPlayer() {
audioList.setItemChecked(which, true)
}
trackDialog.cancel_btt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
trackDialog.dismissSafe(activity)
}
trackDialog.apply_btt?.setOnClickListener {
binding.applyBtt.setOnClickListener {
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
player.setPreferredAudioTrack(
currentTrack?.language, currentTrack?.id
@ -889,7 +889,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
private fun noLinksFound() {
showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
activity?.popCurrentPage()
}
@ -1030,8 +1030,10 @@ class GeneratorPlayer : FullScreenPlayer() {
if (meta.tvType.isAnimeOp()) isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
}
}
player_skip_op?.isVisible = isOpVisible
player_skip_episode?.isVisible = !isOpVisible && viewModel.hasNextEpisode() == true
playerBinding?.playerSkipOp?.isVisible = isOpVisible
playerBinding?.playerSkipEpisode?.isVisible =
!isOpVisible && viewModel.hasNextEpisode() == true
if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) {
viewModel.preLoadNextLinks()
@ -1168,7 +1170,7 @@ class GeneratorPlayer : FullScreenPlayer() {
//Hide title, if set in setting
if (limitTitle < 0) {
player_video_title?.visibility = View.GONE
playerBinding?.playerVideoTitle?.visibility = View.GONE
} else {
//Truncate video title if it exceeds limit
val differenceInLength = playerVideoTitle.length - limitTitle
@ -1179,8 +1181,8 @@ class GeneratorPlayer : FullScreenPlayer() {
}
val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller
player_episode_filler_holder?.isVisible = isFiller ?: false
player_video_title?.text = playerVideoTitle
playerBinding?.playerEpisodeFillerHolder?.isVisible = isFiller ?: false
playerBinding?.playerVideoTitle?.text = playerVideoTitle
}
@SuppressLint("SetTextI18n")
@ -1201,8 +1203,10 @@ class GeneratorPlayer : FullScreenPlayer() {
3 -> "$source - $extra"
else -> ""
}
player_video_title_rez?.text = title
player_video_title_rez?.isVisible = title.isNotBlank()
playerBinding?.playerVideoTitleRez?.apply {
text = title
isVisible = title.isNotBlank()
}
}
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
@ -1230,7 +1234,14 @@ class GeneratorPlayer : FullScreenPlayer() {
unwrapBundle(savedInstanceState)
unwrapBundle(arguments)
return super.onCreateView(inflater, container, savedInstanceState)
val root = super.onCreateView(inflater, container, savedInstanceState) ?: return null
binding = FragmentPlayerBinding.bind(root)
return root
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
var timestampShowState = false
@ -1243,7 +1254,7 @@ class GeneratorPlayer : FullScreenPlayer() {
skipIndex++
println("displayTimeStamp = $show")
timestampShowState = show
skip_chapter_button?.apply {
playerBinding?.skipChapterButton?.apply {
val showWidth = 170.toPx
val noShowWidth = 10.toPx
//if((show && width == showWidth) || (!show && width == noShowWidth)) {
@ -1263,7 +1274,7 @@ class GeneratorPlayer : FullScreenPlayer() {
from, to
).apply {
addListener(onEnd = {
if (!show) skip_chapter_button?.isVisible = false
if (!show) playerBinding?.skipChapterButton?.isVisible = false
})
addUpdateListener { valueAnimator ->
val value = valueAnimator.animatedValue as Int
@ -1283,10 +1294,11 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
if (timestamp != null) {
skip_chapter_button.setText(timestamp.uiText)
println("timestamp: $timestamp")
playerBinding?.skipChapterButton?.setText(timestamp.uiText)
displayTimeStamp(true)
val currentIndex = skipIndex
skip_chapter_button?.handler?.postDelayed({
playerBinding?.skipChapterButton?.handler?.postDelayed({
if (skipIndex == currentIndex)
displayTimeStamp(false)
}, 6000)
@ -1329,11 +1341,11 @@ class GeneratorPlayer : FullScreenPlayer() {
viewModel.loadLinks()
}
overlay_loading_skip_button?.setOnClickListener {
binding?.overlayLoadingSkipButton?.setOnClickListener {
startPlayer()
}
player_loading_go_back?.setOnClickListener {
binding?.playerLoadingGoBack?.setOnClickListener {
player.release()
activity?.popCurrentPage()
}
@ -1357,7 +1369,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
is Resource.Failure -> {
showToast(activity, it.errorString, Toast.LENGTH_LONG)
showToast(it.errorString, Toast.LENGTH_LONG)
startPlayer()
}
}
@ -1366,8 +1378,8 @@ class GeneratorPlayer : FullScreenPlayer() {
observe(viewModel.currentLinks) {
currentLinks = it
val turnVisible = it.isNotEmpty()
val wasGone = overlay_loading_skip_button?.isGone == true
overlay_loading_skip_button?.isVisible = turnVisible
val wasGone = binding?.overlayLoadingSkipButton?.isGone == true
binding?.overlayLoadingSkipButton?.isVisible = turnVisible
normalSafeApiCall {
if (currentLinks.any { link ->
@ -1380,7 +1392,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}
if (turnVisible && wasGone) {
overlay_loading_skip_button?.requestFocus()
binding?.overlayLoadingSkipButton?.requestFocus()
}
}

View file

@ -1,158 +0,0 @@
package com.lagradost.cloudstream3.ui.player
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.result.ResultEpisode
import com.lagradost.cloudstream3.ui.result.getDisplayPosition
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.player_episodes_large.view.episode_holder_large
import kotlinx.android.synthetic.main.player_episodes_large.view.episode_progress
import kotlinx.android.synthetic.main.player_episodes_small.view.episode_holder
import kotlinx.android.synthetic.main.result_episode_large.view.*
data class PlayerEpisodeClickEvent(val action: Int, val data: Any)
class PlayerEpisodeAdapter(
private val items: MutableList<Any> = mutableListOf(),
private val clickCallback: (PlayerEpisodeClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PlayerEpisodeCardViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.player_episodes, parent, false),
clickCallback,
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
println("HOLDER $holder $position")
when (holder) {
is PlayerEpisodeCardViewHolder -> {
holder.bind(items[position])
}
}
}
override fun getItemCount(): Int {
return items.size
}
fun updateList(newList: List<Any>) {
println("Updated list $newList")
val diffResult = DiffUtil.calculateDiff(EpisodeDiffCallback(this.items, newList))
items.clear()
items.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
class PlayerEpisodeCardViewHolder
constructor(
itemView: View,
private val clickCallback: (PlayerEpisodeClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView) {
@SuppressLint("SetTextI18n")
fun bind(card: Any) {
if (card is ResultEpisode) {
val (parentView, otherView) = if (card.poster == null) {
itemView.episode_holder to itemView.episode_holder_large
} else {
itemView.episode_holder_large to itemView.episode_holder
}
val episodeText: TextView? = parentView.episode_text
val episodeFiller: MaterialButton? = parentView.episode_filler
val episodeRating: TextView? = parentView.episode_rating
val episodeDescript: TextView? = parentView.episode_descript
val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress
val episodePoster: ImageView? = parentView.episode_poster
parentView.isVisible = true
otherView.isVisible = false
episodeText?.apply {
val name =
if (card.name == null) "${context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
text = name
isSelected = true
}
episodeFiller?.isVisible = card.isFiller == true
val displayPos = card.getDisplayPosition()
episodeProgress?.max = (card.duration / 1000).toInt()
episodeProgress?.progress = (displayPos / 1000).toInt()
episodeProgress?.isVisible = displayPos > 0L
episodePoster?.isVisible = episodePoster?.setImage(card.poster) == true
if (card.rating != null) {
episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format)
?.format(card.rating.toFloat() / 10f)
} else {
episodeRating?.text = ""
}
episodeRating?.isGone = episodeRating?.text.isNullOrBlank()
episodeDescript?.apply {
text = card.description.html()
isGone = text.isNullOrBlank()
//setOnClickListener {
// clickCallback.invoke(PlayerEpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
//}
}
parentView.setOnClickListener {
clickCallback.invoke(PlayerEpisodeClickEvent(0, card))
}
if (isTrueTvSettings()) {
parentView.isFocusable = true
parentView.isFocusableInTouchMode = true
parentView.touchscreenBlocksFocus = false
}
}
}
}
}
class EpisodeDiffCallback(
private val oldList: List<Any>,
private val newList: List<Any>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val a = oldList[oldItemPosition]
val b = newList[newItemPosition]
return if (a is ResultEpisode && b is ResultEpisode) {
a.id == b.id
} else {
a == b
}
}
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}

View file

@ -1,14 +1,10 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerPrioritizeItemBinding
import com.lagradost.cloudstream3.utils.AppUtils
import kotlinx.android.synthetic.main.player_prioritize_item.view.*
data class SourcePriority<T>(
val data: T,
@ -20,7 +16,8 @@ class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
AppUtils.DiffAdapter<SourcePriority<T>>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return PriorityViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false)
PlayerPrioritizeItemBinding.inflate(LayoutInflater.from(parent.context),parent,false),
//LayoutInflater.from(parent.context).inflate(R.layout.player_prioritize_item, parent, false)
)
}
@ -31,27 +28,27 @@ class PriorityAdapter<T>(override val items: MutableList<SourcePriority<T>>) :
}
class PriorityViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
val binding: PlayerPrioritizeItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
fun <T> bind(item: SourcePriority<T>) {
val plusButton: ImageView = itemView.add_button
/* val plusButton: ImageView = itemView.add_button
val subtractButton: ImageView = itemView.subtract_button
val priorityText: TextView = itemView.priority_text
val priorityNumber: TextView = itemView.priority_number
priorityText.text = item.name
val priorityNumber: TextView = itemView.priority_number*/
binding.priorityText.text = item.name
fun updatePriority() {
priorityNumber.text = item.priority.toString()
binding.priorityNumber.text = item.priority.toString()
}
updatePriority()
plusButton.setOnClickListener {
binding.addButton.setOnClickListener {
// If someone clicks til the integer limit then they deserve to crash.
item.priority++
updatePriority()
}
subtractButton.setOnClickListener {
binding.subtractButton.setOnClickListener {
item.priority--
updatePriority()
}

View file

@ -8,19 +8,13 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerQualityProfileItemBinding
import com.lagradost.cloudstream3.ui.result.UiImage
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.player_quality_profile_item.view.card_view
import kotlinx.android.synthetic.main.player_quality_profile_item.view.outline
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_image_background
import kotlinx.android.synthetic.main.player_quality_profile_item.view.profile_text
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_mobile_data
import kotlinx.android.synthetic.main.player_quality_profile_item.view.text_is_wifi
class ProfilesAdapter(
override val items: MutableList<QualityDataHelper.QualityProfile>,
@ -34,8 +28,9 @@ class ProfilesAdapter(
}) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ProfilesViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.player_quality_profile_item, parent, false)
PlayerQualityProfileItemBinding.inflate(LayoutInflater.from(parent.context),parent,false)
//LayoutInflater.from(parent.context)
// .inflate(R.layout.player_quality_profile_item, parent, false)
)
}
@ -52,8 +47,8 @@ class ProfilesAdapter(
}
inner class ProfilesViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
val binding: PlayerQualityProfileItemBinding,
) : RecyclerView.ViewHolder(binding.root) {
private val art = listOf(
R.drawable.profile_bg_teal,
R.drawable.profile_bg_blue,
@ -65,12 +60,12 @@ class ProfilesAdapter(
)
fun bind(item: QualityDataHelper.QualityProfile, index: Int) {
val priorityText: TextView = itemView.profile_text
val profileBg: ImageView = itemView.profile_image_background
val wifiText: TextView = itemView.text_is_wifi
val dataText: TextView = itemView.text_is_mobile_data
val outline: View = itemView.outline
val cardView: View = itemView.card_view
val priorityText: TextView = binding.profileText
val profileBg: ImageView = binding.profileImageBackground
val wifiText: TextView = binding.textIsWifi
val dataText: TextView = binding.textIsMobileData
val outline: View = binding.outline
val cardView: View = binding.cardView
priorityText.text = item.name.asString(itemView.context)
dataText.isVisible = item.type == QualityDataHelper.QualityProfileType.Data

View file

@ -1,20 +1,16 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.app.Dialog
import android.view.View
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerQualityProfileDialogBinding
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlinx.android.synthetic.main.player_quality_profile_dialog.*
class QualityProfileDialog(
val activity: FragmentActivity,
@ -24,83 +20,86 @@ class QualityProfileDialog(
private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit
) : Dialog(activity, themeRes) {
override fun show() {
setContentView(R.layout.player_quality_profile_dialog)
val profilesRecyclerView: RecyclerView = profiles_recyclerview
val binding = PlayerQualityProfileDialogBinding.inflate(this.layoutInflater, null, false)
setContentView(binding.root)//R.layout.player_quality_profile_dialog)
/*val profilesRecyclerView: RecyclerView = profiles_recyclerview
val useBtt: View = use_btt
val editBtt: View = edit_btt
val cancelBtt: View = cancel_btt
val defaultBtt: View = set_default_btt
val currentProfileText: TextView = currently_selected_profile_text
val selectedItemActionsHolder: View = selected_item_holder
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile()
}
fun refreshProfiles() {
currentProfileText.text = getProfileName(usedProfile).asString(context)
(profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles())
}
profilesRecyclerView.adapter = ProfilesAdapter(
mutableListOf(),
usedProfile,
) { oldIndex: Int?, newIndex: Int ->
profilesRecyclerView.adapter?.notifyItemChanged(newIndex)
selectedItemActionsHolder.alpha = 1f
if (oldIndex != null) {
profilesRecyclerView.adapter?.notifyItemChanged(oldIndex)
val selectedItemActionsHolder: View = selected_item_holder*/
binding.apply {
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
return (profilesRecyclerview.adapter as? ProfilesAdapter)?.getCurrentProfile()
}
}
refreshProfiles()
editBtt.setOnClickListener {
getCurrentProfile()?.let { profile ->
SourcePriorityDialog(context, themeRes, links, profile) {
refreshProfiles()
}.show()
fun refreshProfiles() {
currentlySelectedProfileText.text = getProfileName(usedProfile).asString(context)
(profilesRecyclerview.adapter as? ProfilesAdapter)?.updateList(getProfiles())
}
profilesRecyclerview.adapter = ProfilesAdapter(
mutableListOf(),
usedProfile,
) { oldIndex: Int?, newIndex: Int ->
profilesRecyclerview.adapter?.notifyItemChanged(newIndex)
selectedItemHolder.alpha = 1f
if (oldIndex != null) {
profilesRecyclerview.adapter?.notifyItemChanged(oldIndex)
}
}
refreshProfiles()
editBtt.setOnClickListener {
getCurrentProfile()?.let { profile ->
SourcePriorityDialog(context, themeRes, links, profile) {
refreshProfiles()
}.show()
}
}
}
defaultBtt.setOnClickListener {
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
val choices = QualityDataHelper.QualityProfileType.values()
.filter { it != QualityDataHelper.QualityProfileType.None }
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
setDefaultBtt.setOnClickListener {
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
val choices = QualityDataHelper.QualityProfileType.values()
.filter { it != QualityDataHelper.QualityProfileType.None }
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
activity.showBottomDialog(
choiceNames,
choices.indexOf(currentProfile.type),
txt(R.string.set_default).asString(context),
false,
{},
{ index ->
val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog
// Remove previous picks
if (pickedChoice.unique) {
getProfiles().filter { it.type == pickedChoice }.forEach {
QualityDataHelper.setQualityProfileType(it.id, null)
activity.showBottomDialog(
choiceNames,
choices.indexOf(currentProfile.type),
txt(R.string.set_default).asString(context),
false,
{},
{ index ->
val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog
// Remove previous picks
if (pickedChoice.unique) {
getProfiles().filter { it.type == pickedChoice }.forEach {
QualityDataHelper.setQualityProfileType(it.id, null)
}
}
}
QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice)
refreshProfiles()
})
}
QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice)
refreshProfiles()
})
}
cancelBtt.setOnClickListener {
this.dismissSafe()
}
cancelBtt.setOnClickListener {
this@QualityProfileDialog.dismissSafe()
}
useBtt.setOnClickListener {
getCurrentProfile()?.let {
profileSelectionCallback.invoke(it)
this.dismissSafe()
useBtt.setOnClickListener {
getCurrentProfile()?.let {
profileSelectionCallback.invoke(it)
this@QualityProfileDialog.dismissSafe()
}
}
}
super.show()
}
}

View file

@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.player.source_priority
import android.app.Dialog
import android.content.Context
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.view.LayoutInflater
import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.work.impl.constraints.controllers.ConstraintController
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerSelectSourcePriorityBinding
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlinx.android.synthetic.main.player_select_source_priority.*
class SourcePriorityDialog(
ctx: Context,
val ctx: Context,
@StyleRes themeRes: Int,
val links: List<ExtractorLink>,
private val profile: QualityDataHelper.QualityProfile,
@ -30,13 +24,14 @@ class SourcePriorityDialog(
private val updatedCallback: () -> Unit
) : Dialog(ctx, themeRes) {
override fun show() {
setContentView(R.layout.player_select_source_priority)
val sourcesRecyclerView: RecyclerView = sort_sources
val qualitiesRecyclerView: RecyclerView = sort_qualities
val profileText: EditText = profile_text_editable
val saveBtt: View = save_btt
val exitBtt: View = close_btt
val helpBtt: View = help_btt
val binding = PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false)
setContentView(binding.root)
val sourcesRecyclerView = binding.sortSources
val qualitiesRecyclerView = binding.sortQualities
val profileText = binding.profileTextEditable
val saveBtt = binding.saveBtt
val exitBtt = binding.closeBtt
val helpBtt = binding.helpBtt
profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context))
profileText.hint = txt(R.string.profile_number, profile.id).asString(context)

View file

@ -19,8 +19,10 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.QuickSearchBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
@ -37,7 +39,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.quick_search.*
import java.util.concurrent.locks.ReentrantLock
class QuickSearchFragment : Fragment() {
@ -45,6 +46,13 @@ class QuickSearchFragment : Fragment() {
const val AUTOSEARCH_KEY = "autosearch"
const val PROVIDER_KEY = "providers"
fun pushSearch(
autoSearch: String? = null,
providers: Array<String>? = null
) {
pushSearch(activity, autoSearch, providers)
}
fun pushSearch(
activity: Activity?,
autoSearch: String? = null,
@ -72,6 +80,8 @@ class QuickSearchFragment : Fragment() {
private var providers: Set<String>? = null
private lateinit var searchViewModel: SearchViewModel
var binding: QuickSearchBinding? = null
private var bottomSheetDialog: BottomSheetDialog? = null
@ -79,13 +89,21 @@ class QuickSearchFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
activity?.window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
)
searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
bottomSheetDialog?.ownShow()
return inflater.inflate(R.layout.quick_search, container, false)
val localBinding = QuickSearchBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.quick_search, container, false)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onDestroy() {
@ -111,7 +129,7 @@ class QuickSearchFragment : Fragment() {
activity?.getSpanCount()?.let {
HomeFragment.currentSpan = it
}
quick_search_autofit_results.spanCount = HomeFragment.currentSpan
binding?.quickSearchAutofitResults?.spanCount = HomeFragment.currentSpan
HomeFragment.currentSpan = HomeFragment.currentSpan
HomeFragment.configEvent.invoke(HomeFragment.currentSpan)
}
@ -123,7 +141,7 @@ class QuickSearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(quick_search_root)
fixPaddingStatusbar(binding?.quickSearchRoot)
fixGrid()
arguments?.getStringArray(PROVIDER_KEY)?.let {
@ -136,23 +154,25 @@ class QuickSearchFragment : Fragment() {
} else false
if (isSingleProvider) {
quick_search_autofit_results.adapter = activity?.let {
SearchAdapter(
binding?.quickSearchAutofitResults?.apply {
adapter = SearchAdapter(
ArrayList(),
quick_search_autofit_results,
this,
) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
SearchHelper.handleSearchClickCallback(callback)
}
}
try {
quick_search?.queryHint = getString(R.string.search_hint_site).format(providers?.first())
binding?.quickSearch?.queryHint =
getString(R.string.search_hint_site).format(providers?.first())
} catch (e: Exception) {
logError(e)
}
} else {
quick_search_master_recycler?.adapter =
binding?.quickSearchMasterRecycler?.adapter =
ParentItemAdapter(mutableListOf(), { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
SearchHelper.handleSearchClickCallback(callback)
//when (callback.action) {
//SEARCH_ACTION_LOAD -> {
// clickCallback?.invoke(callback)
@ -164,18 +184,17 @@ class QuickSearchFragment : Fragment() {
bottomSheetDialog = null
})
})
quick_search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
binding?.quickSearchMasterRecycler?.layoutManager = GridLayoutManager(context, 1)
}
quick_search_autofit_results?.isVisible = isSingleProvider
quick_search_master_recycler?.isGone = isSingleProvider
binding?.quickSearchAutofitResults?.isVisible = isSingleProvider
binding?.quickSearchMasterRecycler?.isGone = isSingleProvider
val listLock = ReentrantLock()
observe(searchViewModel.currentSearch) { list ->
try {
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
listLock.lock()
(quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
(binding?.quickSearchMasterRecycler?.adapter as ParentItemAdapter?)?.apply {
updateList(list.map { ongoing ->
val ongoingList = HomePageList(
ongoing.apiName,
@ -192,19 +211,18 @@ class QuickSearchFragment : Fragment() {
}
val searchExitIcon =
quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
binding?.quickSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
//val searchMagIcon =
// quick_search?.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
// binding.quickSearch.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
//searchMagIcon?.scaleX = 0.65f
//searchMagIcon?.scaleY = 0.65f
quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
if (search(context, query, false))
UIHelper.hideKeyboard(quick_search)
UIHelper.hideKeyboard(binding?.quickSearch)
return true
}
@ -214,27 +232,28 @@ class QuickSearchFragment : Fragment() {
return true
}
})
quick_search_loading_bar.alpha = 0f
binding?.quickSearchLoadingBar?.alpha = 0f
observe(searchViewModel.searchResponse) {
when (it) {
is Resource.Success -> {
it.value.let { data ->
(quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList(
(binding?.quickSearchAutofitResults?.adapter as? SearchAdapter)?.updateList(
context?.filterSearchResultByFilmQuality(data) ?: data
)
}
searchExitIcon?.alpha = 1f
quick_search_loading_bar?.alpha = 0f
binding?.quickSearchLoadingBar?.alpha = 0f
}
is Resource.Failure -> {
// Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show()
searchExitIcon?.alpha = 1f
quick_search_loading_bar?.alpha = 0f
binding?.quickSearchLoadingBar?.alpha = 0f
}
is Resource.Loading -> {
searchExitIcon?.alpha = 0f
quick_search_loading_bar?.alpha = 1f
binding?.quickSearchLoadingBar?.alpha = 1f
}
}
}
@ -246,13 +265,12 @@ class QuickSearchFragment : Fragment() {
// UIHelper.showInputMethod(view.findFocus())
// }
//}
quick_search_back.setOnClickListener {
binding?.quickSearchBack?.setOnClickListener {
activity?.popCurrentPage()
}
arguments?.getString(AUTOSEARCH_KEY)?.let {
quick_search?.setQuery(it, true)
binding?.quickSearch?.setQuery(it, true)
arguments?.remove(AUTOSEARCH_KEY)
}
}

View file

@ -3,18 +3,16 @@ package com.lagradost.cloudstream3.ui.result
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.ActorData
import com.lagradost.cloudstream3.ActorRole
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.CastItemBinding
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.cast_item.view.*
class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
data class ActorMetaData(
var isInverted: Boolean,
val actor: ActorData,
@ -24,7 +22,7 @@ class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.cast_item, parent, false),
CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback
)
}
@ -68,15 +66,10 @@ class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private class CardViewHolder
constructor(
itemView: View,
val binding: CastItemBinding,
private val focusCallback : (View?) -> Unit = {}
) :
RecyclerView.ViewHolder(itemView) {
private val actorImage: ImageView = itemView.actor_image
private val actorName: TextView = itemView.actor_name
private val actorExtra: TextView = itemView.actor_extra
private val voiceActorImage: ImageView = itemView.voice_actor_image
private val voiceActorImageHolder: View = itemView.voice_actor_image_holder
private val voiceActorName: TextView = itemView.voice_actor_name
RecyclerView.ViewHolder(binding.root) {
fun bind(actor: ActorData, isInverted: Boolean, position: Int, callback: (Int) -> Unit) {
val (mainImg, vaImage) = if (!isInverted || actor.voiceActor?.image.isNullOrBlank()) {
@ -85,43 +78,53 @@ class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
Pair(actor.voiceActor?.image, actor.actor.image)
}
itemView.setOnFocusChangeListener { v, hasFocus ->
if(hasFocus) {
focusCallback(v)
}
}
itemView.setOnClickListener {
callback(position)
}
actorImage.setImage(mainImg)
binding.apply {
actorImage.setImage(mainImg)
actorName.text = actor.actor.name
actor.role?.let {
actorExtra.context?.getString(
when (it) {
ActorRole.Main -> {
R.string.actor_main
}
ActorRole.Supporting -> {
R.string.actor_supporting
}
ActorRole.Background -> {
R.string.actor_background
actorName.text = actor.actor.name
actor.role?.let {
actorExtra.context?.getString(
when (it) {
ActorRole.Main -> {
R.string.actor_main
}
ActorRole.Supporting -> {
R.string.actor_supporting
}
ActorRole.Background -> {
R.string.actor_background
}
}
)?.let { text ->
actorExtra.isVisible = true
actorExtra.text = text
}
)?.let { text ->
} ?: actor.roleString?.let {
actorExtra.isVisible = true
actorExtra.text = text
actorExtra.text = it
} ?: run {
actorExtra.isVisible = false
}
} ?: actor.roleString?.let {
actorExtra.isVisible = true
actorExtra.text = it
} ?: run {
actorExtra.isVisible = false
}
if (actor.voiceActor == null) {
voiceActorImageHolder.isVisible = false
voiceActorName.isVisible = false
} else {
voiceActorName.text = actor.voiceActor.name
voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage)
if (actor.voiceActor == null) {
voiceActorImageHolder.isVisible = false
voiceActorName.isVisible = false
} else {
voiceActorName.text = actor.voiceActor.name
voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage)
}
}
}
}

View file

@ -3,34 +3,24 @@ package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding
import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.android.synthetic.main.result_episode.view.*
import kotlinx.android.synthetic.main.result_episode.view.episode_text
import kotlinx.android.synthetic.main.result_episode_large.view.*
import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler
import kotlinx.android.synthetic.main.result_episode_large.view.episode_progress
import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_download
import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_progress_downloaded
import java.util.*
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
@ -59,7 +49,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
const val ACTION_PLAY_EPISODE_IN_MPV = 17
const val ACTION_MARK_AS_WATCHED = 18
const val TV_EP_SIZE_LARGE = 400
const val TV_EP_SIZE_SMALL = 300
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
class EpisodeAdapter(
@ -88,49 +79,10 @@ class EpisodeAdapter(
var cardList: MutableList<ResultEpisode> = mutableListOf()
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
return Collections.unmodifiableSet(mBoundViewHolders)
}
fun killAdapter() {
getAllBoundViewHolders()?.forEach { view ->
view?.downloadButton?.dispose()
}
}
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if (holder.itemView.hasFocus()) {
holder.itemView.clearFocus()
}
//(holder.itemView as? FrameLayout?)?.descendantFocusability =
// ViewGroup.FOCUS_BLOCK_DESCENDANTS
if (holder is DownloadButtonViewHolder) {
holder.downloadButton.dispose()
}
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
holder.downloadButton.dispose()
mBoundViewHolders.remove(holder)
//(holder.itemView as? FrameLayout?)?.descendantFocusability =
// ViewGroup.FOCUS_BLOCK_DESCENDANTS
}
}
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
if (holder is DownloadButtonViewHolder) {
//println("onViewAttachedToWindow = ${holder.absoluteAdapterPosition}")
//holder.itemView.post {
// if (holder.itemView.isAttachedToWindow)
// (holder.itemView as? FrameLayout?)?.descendantFocusability =
// ViewGroup.FOCUS_AFTER_DESCENDANTS
//}
holder.reattachDownloadButton()
}
}
fun updateList(newList: List<ResultEpisode>) {
@ -144,27 +96,62 @@ class EpisodeAdapter(
diffResult.dispatchUpdatesTo(this)
}
var layout = R.layout.result_episode_both
private fun getItem(position: Int): ResultEpisode {
return cardList[position]
}
override fun getItemViewType(position: Int): Int {
val item = getItem(position)
return if (item.poster.isNullOrBlank()) 0 else 1
}
// private val layout = R.layout.result_episode_both
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
/*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2)
R.layout.result_episode_large
else R.layout.result_episode*/
return EpisodeCardViewHolder(
LayoutInflater.from(parent.context)
.inflate(layout, parent, false),
hasDownloadSupport,
clickCallback,
downloadClickCallback
)
return when (viewType) {
0 -> {
EpisodeCardViewHolderSmall(
ResultEpisodeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
hasDownloadSupport,
clickCallback,
downloadClickCallback
)
}
1 -> {
EpisodeCardViewHolderLarge(
ResultEpisodeLargeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
hasDownloadSupport,
clickCallback,
downloadClickCallback
)
}
else -> throw NotImplementedError()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is EpisodeCardViewHolder -> {
holder.bind(cardList[position])
mBoundViewHolders.add(holder)
is EpisodeCardViewHolderLarge -> {
holder.bind(getItem(position))
}
is EpisodeCardViewHolderSmall -> {
holder.bind(getItem(position))
}
}
}
@ -173,94 +160,108 @@ class EpisodeAdapter(
return cardList.size
}
class EpisodeCardViewHolder
class EpisodeCardViewHolderLarge
constructor(
itemView: View,
val binding: ResultEpisodeLargeBinding,
private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
override var downloadButton = EasyDownloadButton()
var episodeDownloadBar: ContentLoadingProgressBar? = null
var episodeDownloadImage: ImageView? = null
) : RecyclerView.ViewHolder(binding.root) {
var localCard: ResultEpisode? = null
@SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) {
localCard = card
val setWidth =
if (isTvSettings()) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT
binding.episodeLinHolder.layoutParams.width = setWidth
binding.episodeHolderLarge.layoutParams.width = setWidth
binding.episodeHolder.layoutParams.width = setWidth
val isTrueTv = isTrueTvSettings()
val (parentView, otherView) = if (card.poster == null) {
itemView.episode_holder to itemView.episode_holder_large
} else {
itemView.episode_holder_large to itemView.episode_holder
}
parentView.isVisible = true
otherView.isVisible = false
binding.apply {
downloadButton.isVisible = hasDownloadSupport
downloadButton.setDefaultClickListener(
VideoDownloadHelper.DownloadEpisodeCached(
card.name,
card.poster,
card.episode,
card.season,
card.id,
card.parentId,
card.rating,
card.description,
System.currentTimeMillis(),
), null
) {
when (it.action) {
DOWNLOAD_ACTION_DOWNLOAD -> {
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
}
val episodeText: TextView = parentView.episode_text
val episodeFiller: MaterialButton? = parentView.episode_filler
val episodeRating: TextView? = parentView.episode_rating
val episodeDescript: TextView? = parentView.episode_descript
val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress
val episodePoster: ImageView? = parentView.episode_poster
DOWNLOAD_ACTION_LONG_CLICK -> {
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card))
}
episodeDownloadBar =
parentView.result_episode_progress_downloaded
episodeDownloadImage = parentView.result_episode_download
val name =
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
episodeFiller?.isVisible = card.isFiller == true
episodeText.text =
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
episodeText.isSelected = true // is needed for text repeating
if (card.videoWatchState == VideoWatchState.Watched) {
// This cannot be done in getDisplayPosition() as when you have not watched something
// the duration and position is 0
episodeProgress?.max = 1
episodeProgress?.progress = 1
episodeProgress?.isVisible = true
} else {
val displayPos = card.getDisplayPosition()
episodeProgress?.max = (card.duration / 1000).toInt()
episodeProgress?.progress = (displayPos / 1000).toInt()
episodeProgress?.isVisible = displayPos > 0L
}
episodePoster?.isVisible = episodePoster?.setImage(card.poster) == true
if (card.rating != null) {
episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format)
?.format(card.rating.toFloat() / 10f)
} else {
episodeRating?.text = ""
}
episodeRating?.isGone = episodeRating?.text.isNullOrBlank()
episodeDescript?.apply {
text = card.description.html()
isGone = text.isNullOrBlank()
setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
}
}
if (!isTrueTv) {
episodePoster?.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
else -> {
downloadClickCallback.invoke(it)
}
}
}
episodePoster?.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card))
return@setOnLongClickListener true
val name =
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
episodeFiller.isVisible = card.isFiller == true
episodeText.text =
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
episodeText.isSelected = true // is needed for text repeating
if (card.videoWatchState == VideoWatchState.Watched) {
// This cannot be done in getDisplayPosition() as when you have not watched something
// the duration and position is 0
episodeProgress.max = 1
episodeProgress.progress = 1
episodeProgress.isVisible = true
} else {
val displayPos = card.getDisplayPosition()
episodeProgress.max = (card.duration / 1000).toInt()
episodeProgress.progress = (displayPos / 1000).toInt()
episodeProgress.isVisible = displayPos > 0L
}
episodePoster.isVisible = episodePoster.setImage(card.poster) == true
if (card.rating != null) {
episodeRating.text = episodeRating.context?.getString(R.string.rated_format)
?.format(card.rating.toFloat() / 10f)
} else {
episodeRating.text = ""
}
episodeRating.isGone = episodeRating.text.isNullOrBlank()
episodeDescript.apply {
text = card.description.html()
isGone = text.isNullOrBlank()
setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
}
}
if (!isTrueTv) {
episodePoster.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
episodePoster.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card))
return@setOnLongClickListener true
}
}
}
itemView.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
@ -276,29 +277,30 @@ class EpisodeAdapter(
return@setOnLongClickListener true
}
episodeDownloadImage?.isVisible = hasDownloadSupport
episodeDownloadBar?.isVisible = hasDownloadSupport
reattachDownloadButton()
//binding.resultEpisodeDownload.isVisible = hasDownloadSupport
//binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport
}
}
override fun reattachDownloadButton() {
downloadButton.dispose()
val card = localCard
if (hasDownloadSupport && card != null) {
if (episodeDownloadBar == null ||
episodeDownloadImage == null
) return
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
itemView.context,
card.id
)
class EpisodeCardViewHolderSmall
constructor(
val binding: ResultEpisodeBinding,
private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) {
val isTrueTv = isTrueTvSettings()
downloadButton.setUpButton(
downloadInfo?.fileLength,
downloadInfo?.totalBytes,
episodeDownloadBar ?: return,
episodeDownloadImage ?: return,
null,
binding.episodeHolder.layoutParams.apply {
width =
if (isTvSettings()) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT
}
binding.apply {
downloadButton.isVisible = hasDownloadSupport
downloadButton.setDefaultClickListener(
VideoDownloadHelper.DownloadEpisodeCached(
card.name,
card.poster,
@ -309,14 +311,60 @@ class EpisodeAdapter(
card.rating,
card.description,
System.currentTimeMillis(),
)
), null
) {
if (it.action == DOWNLOAD_ACTION_DOWNLOAD) {
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
} else {
downloadClickCallback.invoke(it)
when (it.action) {
DOWNLOAD_ACTION_DOWNLOAD -> {
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
}
DOWNLOAD_ACTION_LONG_CLICK -> {
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card))
}
else -> {
downloadClickCallback.invoke(it)
}
}
}
val name =
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
episodeFiller.isVisible = card.isFiller == true
episodeText.text =
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
episodeText.isSelected = true // is needed for text repeating
if (card.videoWatchState == VideoWatchState.Watched) {
// This cannot be done in getDisplayPosition() as when you have not watched something
// the duration and position is 0
episodeProgress.max = 1
episodeProgress.progress = 1
episodeProgress.isVisible = true
} else {
val displayPos = card.getDisplayPosition()
episodeProgress.max = (card.duration / 1000).toInt()
episodeProgress.progress = (displayPos / 1000).toInt()
episodeProgress.isVisible = displayPos > 0L
}
itemView.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
}
if (isTrueTv) {
itemView.isFocusable = true
itemView.isFocusableInTouchMode = true
//itemView.touchscreenBlocksFocus = false
}
itemView.setOnLongClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
return@setOnLongClickListener true
}
//binding.resultEpisodeDownload.isVisible = hasDownloadSupport
//binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport
}
}
}

View file

@ -1,11 +1,10 @@
package com.lagradost.cloudstream3.ui.result
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
/*
@ -24,7 +23,6 @@ const val IMAGE_CLICK = 0
const val IMAGE_LONG_CLICK = 1
class ImageAdapter(
val layout: Int,
val clickCallback: ((Int) -> Unit)? = null,
val nextFocusUp: Int? = null,
val nextFocusDown: Int? = null,
@ -34,7 +32,9 @@ class ImageAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ImageViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false)
//result_mini_image
ResultMiniImageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
// LayoutInflater.from(parent.context).inflate(layout, parent, false)
)
}
@ -66,15 +66,15 @@ class ImageAdapter(
}
class ImageViewHolder
constructor(itemView: View) :
RecyclerView.ViewHolder(itemView) {
constructor(val binding: ResultMiniImageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
img: Int,
clickCallback: ((Int) -> Unit)?,
nextFocusUp: Int?,
nextFocusDown: Int?,
) {
(itemView as? ImageView?)?.apply {
binding.root.apply {
setImageResource(img)
if (nextFocusDown != null) {
this.nextFocusDownId = nextFocusDown

View file

@ -8,9 +8,10 @@ import com.lagradost.cloudstream3.mvvm.logError
fun RecyclerView?.setLinearListLayout(isHorizontal: Boolean = true) {
if (this == null) return
this.layoutManager =
this.context?.let { LinearListLayout(it).apply { if (isHorizontal) setHorizontal() else setVertical() } }
?: this.layoutManager
// ?: this.layoutManager
}
open class LinearListLayout(context: Context?) :
@ -66,7 +67,12 @@ open class LinearListLayout(context: Context?) :
(focused.parent as? RecyclerView)?.focusSearch(direction)
return null
}
if (direction == View.FOCUS_RIGHT) 1 else -1
var ret = if (direction == View.FOCUS_RIGHT) 1 else -1
// only flip on horizontal layout
if (this.isLayoutRTL) {
ret = -ret
}
ret
} else {
if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null
if (direction == View.FOCUS_DOWN) 1 else -1
@ -76,6 +82,13 @@ open class LinearListLayout(context: Context?) :
getPosition(getCorrectParent(focused))?.let { position ->
val lookfor = dir + position
//clamp(dir + position, 0, recyclerView.adapter?.itemCount ?: return null)
// refocus on the same view if going out of bounds, note that we only do it
// for out of bounds one way as we may override the start where item == -1
if (lookfor >= itemCount) {
return getViewFromPos(itemCount - 1) ?: focused
}
getViewFromPos(lookfor) ?: run {
scrollToPosition(lookfor)
null

View file

@ -1,33 +1,81 @@
package com.lagradost.cloudstream3.ui.result
import android.animation.Animator
import android.annotation.SuppressLint
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.ResourceSome
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData
import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.fragment_result_tv.*
import com.lagradost.cloudstream3.utils.UIHelper.setImage
class ResultFragmentTv : ResultFragment() {
override val resultLayout = R.layout.fragment_result_tv
class ResultFragmentTv : Fragment() {
protected lateinit var viewModel: ResultViewModel2
private var binding: FragmentResultTvBinding? = null
override fun onDestroyView() {
binding = null
updateUIEvent -= ::updateUI
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewModel =
ViewModelProvider(this)[ResultViewModel2::class.java]
viewModel.EPISODE_RANGE_SIZE = 50
updateUIEvent += ::updateUI
val localBinding = FragmentResultTvBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
}
private fun updateUI(id: Int?) {
viewModel.reloadEpisodes()
}
private var currentRecommendations: List<SearchResponse> = emptyList()
@ -36,12 +84,15 @@ class ResultFragmentTv : ResultFragment() {
is EpisodeRange -> {
viewModel.changeRange(data)
}
is Int -> {
viewModel.changeSeason(data)
}
is DubStatus -> {
viewModel.changeDubStatus(data)
}
is String -> {
setRecommendations(currentRecommendations, data)
}
@ -66,172 +117,640 @@ class ResultFragmentTv : ResultFragment() {
private fun hasNoFocus(): Boolean {
val focus = activity?.currentFocus
if (focus == null || !focus.isVisible) return true
return focus == this.result_root
return focus == binding?.resultRoot
}
override fun updateEpisodes(episodes: ResourceSome<List<ResultEpisode>>) {
super.updateEpisodes(episodes)
if (episodes is ResourceSome.Success && hasNoFocus()) {
result_episodes?.requestFocus()
}
}
override fun updateMovie(data: ResourceSome<Pair<UiText, ResultEpisode>>) {
super.updateMovie(data)
if (data is ResourceSome.Success && hasNoFocus()) {
result_play_movie?.requestFocus()
}
}
override fun setTrailers(trailers: List<ExtractorLink>?) {
context?.updateHasTrailers()
if (!LoadResponse.isTrailersEnabled) return
result_play_trailer?.isGone = trailers.isNullOrEmpty()
result_play_trailer?.setOnClickListener {
if (trailers.isNullOrEmpty()) return@setOnClickListener
activity.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
ExtractorLinkGenerator(
trailers,
emptyList()
)
)
)
}
}
override fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
currentRecommendations = rec ?: emptyList()
val isInvalid = rec.isNullOrEmpty()
result_recommendations?.isGone = isInvalid
result_recommendations_holder?.isGone = isInvalid
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
(result_recommendations?.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst }
?: emptyList())
binding?.apply {
resultRecommendationsList.isGone = isInvalid
resultRecommendationsHolder.isGone = isInvalid
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
(resultRecommendationsList.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst }
?: emptyList())
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
// very dirty selection
result_recommendations_filter_selection?.isVisible = apiNames.size > 1
result_recommendations_filter_selection?.update(apiNames.map { txt(it) to it })
result_recommendations_filter_selection?.select(apiNames.indexOf(matchAgainst))
} ?: run {
result_recommendations_filter_selection?.isVisible = false
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
// very dirty selection
resultRecommendationsFilterSelection.isVisible = apiNames.size > 1
resultRecommendationsFilterSelection.update(apiNames.map { txt(it) to it })
resultRecommendationsFilterSelection.select(apiNames.indexOf(matchAgainst))
} ?: run {
resultRecommendationsFilterSelection.isVisible = false
}
}
}
var loadingDialog: Dialog? = null
var popupDialog: Dialog? = null
private fun reloadViewModel(forceReload: Boolean) {
if (!viewModel.hasLoaded() || forceReload) {
val storedData = getStoredData() ?: return
viewModel.load(
activity,
storedData.url,
storedData.apiName,
storedData.showFillers,
storedData.dubStatus,
storedData.start
)
}
}
override fun onResume() {
activity?.let {
it.window?.navigationBarColor =
it.colorFromAttribute(R.attr.primaryBlackBackground)
}
afterPluginsLoadedEvent += ::reloadViewModel
super.onResume()
}
override fun onStop() {
afterPluginsLoadedEvent -= ::reloadViewModel
super.onStop()
}
private fun View.fade(turnVisible: Boolean) {
if (turnVisible) {
isVisible = true
}
this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply {
duration = 200
interpolator = DecelerateInterpolator()
setListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator) {
}
override fun onAnimationEnd(animation: Animator) {
this@fade.isVisible = turnVisible
}
override fun onAnimationCancel(animation: Animator) {
}
override fun onAnimationRepeat(animation: Animator) {
}
})
}
this.animate().translationX(if (turnVisible) 0f else if(isRtl()) -100.0f else 100f).apply {
duration = 200
interpolator = DecelerateInterpolator()
}
}
private fun toggleEpisodes(show: Boolean) {
binding?.apply {
episodesShadow.fade(show)
episodeHolderTv.fade(show)
if(episodesShadow.isRtl()) {
episodesShadow.scaleX = -1.0f
episodesShadow.scaleY = -1.0f
} else {
episodesShadow.scaleX = 1.0f
episodesShadow.scaleY = 1.0f
}
}
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
result_episodes?.layoutManager =
//LinearListLayout(result_episodes ?: return, result_episodes?.context).apply {
LinearListLayout(result_episodes?.context).apply {
setHorizontal()
// ===== setup =====
val storedData = getStoredData() ?: return
activity?.window?.decorView?.clearFocus()
activity?.loadCache()
hideKeyboard()
if (storedData.restart || !viewModel.hasLoaded())
viewModel.load(
activity,
storedData.url,
storedData.apiName,
storedData.showFillers,
storedData.dubStatus,
storedData.start
)
// ===== ===== =====
binding?.apply {
//episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f
val leftListener: View.OnFocusChangeListener =
View.OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@OnFocusChangeListener
toggleEpisodes(false)
}
val rightListener: View.OnFocusChangeListener =
View.OnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@OnFocusChangeListener
toggleEpisodes(true)
}
resultPlayMovie.onFocusChangeListener = leftListener
resultPlaySeries.onFocusChangeListener = leftListener
resultResumeSeries.onFocusChangeListener = leftListener
resultPlayTrailer.onFocusChangeListener = leftListener
resultEpisodesShow.onFocusChangeListener = rightListener
resultDescription.onFocusChangeListener = leftListener
resultBookmarkButton.onFocusChangeListener = leftListener
resultEpisodesShow.setOnClickListener {
// toggle, to make it more touch accessable just in case someone thinks that a
// tv layout is better but is using a touch device
toggleEpisodes(!episodeHolderTv.isVisible)
}
// resultEpisodes.onFocusChangeListener = leftListener
redirectToPlay.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@setOnFocusChangeListener
toggleEpisodes(false)
binding?.apply {
val views = listOf(
resultPlayMovie,
resultPlaySeries,
resultResumeSeries,
resultPlayTrailer,
resultBookmarkButton
)
for (requestView in views) {
if (!requestView.isVisible) continue
if (requestView.requestFocus()) break
}
}
}
// parallax on background
resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f
})
redirectToEpisodes.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) return@setOnFocusChangeListener
toggleEpisodes(true)
binding?.apply {
val views = listOf(
resultSeasonSelection,
resultRangeSelection,
resultDubSelection,
resultPlayTrailer,
resultEpisodes
)
for (requestView in views) {
if (!requestView.isShown) continue
if (requestView.requestFocus()) break // View.FOCUS_RIGHT
}
}
}
resultEpisodes.setLinearListLayout(isHorizontal = false)/*.layoutManager =
LinearListLayout(resultEpisodes.context, resultEpisodes.isRtl()).apply {
setVertical()
}*/
resultReloadConnectionerror.setOnClickListener {
viewModel.load(
activity,
storedData.url,
storedData.apiName,
storedData.showFillers,
storedData.dubStatus,
storedData.start
)
}
resultMetaSite.isFocusable = false
//resultReloadConnectionOpenInBrowser.setOnClickListener {view ->
// view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true)
//}
resultSeasonSelection.setAdapter()
resultRangeSelection.setAdapter()
resultDubSelection.setAdapter()
resultRecommendationsFilterSelection.setAdapter()
resultCastItems.setOnFocusChangeListener { _, hasFocus ->
// Always escape focus
if (hasFocus) binding?.resultBookmarkButton?.requestFocus()
}
//resultBack.setOnClickListener {
// activity?.popCurrentPage()
//}
resultRecommendationsList.spanCount = 8
resultRecommendationsList.adapter =
SearchAdapter(
ArrayList(),
resultRecommendationsList,
) { callback ->
if(callback.action == SEARCH_ACTION_FOCUSED)
toggleEpisodes(false)
else
SearchHelper.handleSearchClickCallback(callback)
}
resultEpisodes.adapter =
EpisodeAdapter(
false,
{ episodeClick ->
viewModel.handleAction(episodeClick)
},
{ downloadClickEvent ->
DownloadButtonSetup.handleDownloadClick(downloadClickEvent)
}
)
resultCastItems.layoutManager = object : LinearListLayout(view.context) {
override fun onRequestChildFocus(
parent: RecyclerView,
state: RecyclerView.State,
child: View,
focused: View?
): Boolean {
// Make the cast always focus the first visible item when focused
// from somewhere else. Otherwise it jumps to the last item.
return if (parent.focusedChild == null) {
scrollToPosition(this.findFirstCompletelyVisibleItemPosition())
true
} else {
super.onRequestChildFocus(parent, state, child, focused)
}
}
}.apply {
this.orientation = RecyclerView.HORIZONTAL
}
resultCastItems.adapter = ActorAdaptor {
toggleEpisodes(false)
}
(result_episodes?.adapter as EpisodeAdapter?)?.apply {
layout = R.layout.result_episode_both_tv
}
//result_episodes?.setMaxViewPoolSize(0, Int.MAX_VALUE)
result_season_selection.setAdapter()
result_range_selection.setAdapter()
result_dub_selection.setAdapter()
result_recommendations_filter_selection.setAdapter()
observeNullable(viewModel.resumeWatching) { resume ->
binding?.apply {
// show progress no matter if series or movie
resume?.progress?.let { progress ->
resultResumeSeriesProgressText.setText(progress.progressLeft)
resultResumeSeriesProgress.apply {
isVisible = true
this.max = progress.maxProgress
this.progress = progress.progress
}
resultResumeProgressHolder.isVisible = true
} ?: run {
resultResumeProgressHolder.isVisible = false
}
observe(viewModel.selectPopup) { popup ->
when (popup) {
is Some.Success -> {
popupDialog?.dismissSafe(activity)
// if movie then hide both as movie button is
// always visible on movies, this is done in movie observe
popupDialog = activity?.let { act ->
val pop = popup.value
val options = pop.getOptions(act)
val title = pop.getTitle(act)
if (resume?.isMovie == true) {
resultPlaySeries.isVisible = false
resultResumeSeries.isVisible = false
return@observeNullable
}
act.showBottomDialogInstant(
options, title, {
popupDialog = null
pop.callback(null)
}, {
popupDialog = null
pop.callback(it)
}
// if series then
// > resultPlaySeries is visible when null
// > resultResumeSeries is visible when not null
if (resume == null) {
resultPlaySeries.isVisible = true
resultResumeSeries.isVisible = false
return@observeNullable
}
resultPlaySeries.isVisible = false
resultResumeSeries.isVisible = true
if (hasNoFocus()) {
resultResumeSeries.requestFocus()
}
resultResumeSeries.text =
if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull(
null, // resume.result.name, we don't want episode title
resume.result.episode,
resume.result.season
)
resultResumeSeries.setOnClickListener {
viewModel.handleAction(
EpisodeClickEvent(
storedData.playerAction, //?: ACTION_PLAY_EPISODE_IN_PLAYER,
resume.result
)
)
}
resultResumeSeries.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result)
)
return@setOnLongClickListener true
}
}
}
observe(viewModel.trailers) { trailersLinks ->
context?.updateHasTrailers()
if (!LoadResponse.isTrailersEnabled) return@observe
val trailers = trailersLinks.flatMap { it.mirros }
binding?.resultPlayTrailer?.apply {
isGone = trailers.isEmpty()
setOnClickListener {
if (trailers.isEmpty()) return@setOnClickListener
activity.navigate(
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
ExtractorLinkGenerator(
trailers,
emptyList()
)
)
)
}
}
}
observe(viewModel.watchStatus) { watchType ->
binding?.resultBookmarkButton?.apply {
setText(watchType.stringRes)
setOnClickListener { view ->
activity?.showBottomDialog(
WatchType.values().map { view.context.getString(it.stringRes) }.toList(),
watchType.ordinal,
view.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
}
}
}
}
observeNullable(viewModel.movie) { data ->
binding?.apply {
resultPlayMovie.isVisible = data is Resource.Success
seriesHolder.isVisible = data == null
resultEpisodesShow.isVisible = data == null
(data as? Resource.Success)?.value?.let { (text, ep) ->
resultPlayMovie.setText(text)
resultPlayMovie.setOnClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
)
}
}
is Some.None -> {
popupDialog?.dismissSafe(activity)
popupDialog = null
resultPlayMovie.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep)
)
return@setOnLongClickListener true
}
if (hasNoFocus()) {
resultPlayMovie.requestFocus()
}
}
}
}
observe(viewModel.loadedLinks) { load ->
when (load) {
is Some.Success -> {
if (loadingDialog?.isShowing != true) {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
observeNullable(viewModel.selectPopup) { popup ->
if (popup == null) {
popupDialog?.dismissSafe(activity)
popupDialog = null
return@observeNullable
}
popupDialog?.dismissSafe(activity)
popupDialog = activity?.let { act ->
val options = popup.getOptions(act)
val title = popup.getTitle(act)
act.showBottomDialogInstant(
options, title, {
popupDialog = null
popup.callback(null)
}, {
popupDialog = null
popup.callback(it)
}
loadingDialog = loadingDialog ?: context?.let { ctx ->
val builder = BottomSheetDialog(ctx)
builder.setContentView(R.layout.bottom_loading)
builder.setOnDismissListener {
loadingDialog = null
viewModel.cancelLinks()
}
//builder.setOnCancelListener {
// it?.dismiss()
//}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
}
}
is Some.None -> {
loadingDialog?.dismissSafe(activity)
)
}
}
observeNullable(viewModel.loadedLinks) { load ->
if (load == null) {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
return@observeNullable
}
if (loadingDialog?.isShowing != true) {
loadingDialog?.dismissSafe(activity)
loadingDialog = null
}
loadingDialog = loadingDialog ?: context?.let { ctx ->
val builder = BottomSheetDialog(ctx)
builder.setContentView(R.layout.bottom_loading)
builder.setOnDismissListener {
loadingDialog = null
viewModel.cancelLinks()
}
//builder.setOnCancelListener {
// it?.dismiss()
//}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
}
}
observe(viewModel.episodesCountText) { count ->
result_episodes_text.setText(count)
observeNullable(viewModel.episodesCountText) { count ->
binding?.resultEpisodesText.setText(count)
}
observe(viewModel.selectedRangeIndex) { selected ->
result_range_selection.select(selected)
binding?.resultRangeSelection.select(selected)
}
observe(viewModel.selectedSeasonIndex) { selected ->
result_season_selection.select(selected)
binding?.resultSeasonSelection.select(selected)
}
observe(viewModel.selectedDubStatusIndex) { selected ->
result_dub_selection.select(selected)
binding?.resultDubSelection.select(selected)
}
observe(viewModel.rangeSelections) {
result_range_selection.update(it)
binding?.resultRangeSelection.update(it)
}
observe(viewModel.dubSubSelections) {
result_dub_selection.update(it)
binding?.resultDubSelection.update(it)
}
observe(viewModel.seasonSelections) {
result_season_selection.update(it)
binding?.resultSeasonSelection.update(it)
}
result_back?.setOnClickListener {
activity?.popCurrentPage()
observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null)
}
result_recommendations?.spanCount = 8
result_recommendations?.adapter =
SearchAdapter(
ArrayList(),
result_recommendations,
) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
observe(viewModel.episodeSynopsis) { description ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(description.html())
.setTitle(R.string.synopsis)
.setOnDismissListener {
viewModel.releaseEpisodeSynopsis()
}
.show()
}
}
observeNullable(viewModel.episodes) { episodes ->
binding?.apply {
resultEpisodes.isVisible = episodes is Resource.Success
// resultEpisodeLoading.isVisible = episodes is Resource.Loading
if (episodes is Resource.Success) {
val first = episodes.value.firstOrNull()
if (first != null) {
resultPlaySeries.text = context?.getNameFull(
null, // resume.result.name, we don't want episode title
first.episode,
first.season
)
resultPlaySeries.setOnClickListener {
viewModel.handleAction(
EpisodeClickEvent(
ACTION_PLAY_EPISODE_IN_PLAYER,
first
)
)
}
resultPlaySeries.setOnLongClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_SHOW_OPTIONS, first)
)
return@setOnLongClickListener true
}
}
/*
* Okay so what is this fuckery?
* Basically Android TV will crash if you request a new focus while
* the adapter gets updated.
*
* This means that if you load thumbnails and request a next focus at the same time
* the app will crash without any way to catch it!
*
* How to bypass this?
* This code basically steals the focus for 500ms and puts it in an inescapable view
* then lets out the focus by requesting focus to result_episodes
*/
val hasEpisodes =
!(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty()
/*val focus = activity?.currentFocus
if (hasEpisodes) {
// Make it impossible to focus anywhere else!
temporaryNoFocus.isFocusable = true
temporaryNoFocus.requestFocus()
}*/
(resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value)
/* if (hasEpisodes) main {
delay(500)
// This might make some people sad as it changes the focus when leaving an episode :(
if(focus?.requestFocus() == true) {
temporaryNoFocus.isFocusable = false
return@main
}
temporaryNoFocus.isFocusable = false
temporaryNoFocus.requestFocus()
}
if (hasNoFocus())
binding?.resultEpisodes?.requestFocus()*/
}
}
}
observeNullable(viewModel.page) { data ->
if (data == null) return@observeNullable
binding?.apply {
when (data) {
is Resource.Success -> {
val d = data.value
resultVpn.setText(d.vpnText)
resultInfo.setText(d.metaText)
resultNoEpisodes.setText(d.noEpisodesFoundText)
resultTitle.setText(d.titleText)
resultMetaSite.setText(d.apiName)
resultMetaType.setText(d.typeText)
resultMetaYear.setText(d.yearText)
resultMetaDuration.setText(d.durationText)
resultMetaRating.setText(d.ratingText)
resultCastText.setText(d.actorsText)
resultNextAiring.setText(d.nextAiringEpisode)
resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.setImage(d.posterImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(d.plotText.asString(ctx).html())
.setTitle(d.plotHeaderText.asString(ctx))
.show()
}
}
val error = listOf(
R.drawable.profile_bg_dark_blue,
R.drawable.profile_bg_blue,
R.drawable.profile_bg_orange,
R.drawable.profile_bg_pink,
R.drawable.profile_bg_purple,
R.drawable.profile_bg_red,
R.drawable.profile_bg_teal
).random()
backgroundPoster.setImage(
d.posterBackgroundImage ?: UiImage.Drawable(error),
radius = 0,
errorImageDrawable = error
)
resultComingSoon.isVisible = d.comingSoon
resultDataHolder.isGone = d.comingSoon
UIHelper.populateChips(resultTag, d.tags)
resultCastItems.isGone = d.actors.isNullOrEmpty()
(resultCastItems.adapter as? ActorAdaptor)?.updateList(
d.actors ?: emptyList()
)
}
is Resource.Loading -> {
}
is Resource.Failure -> {
resultErrorText.text =
storedData.url.plus("\n") + data.errorString
}
}
resultFinishLoading.isVisible = data is Resource.Success
resultLoading.isVisible = data is Resource.Loading
resultLoadingError.isVisible = data is Resource.Failure
//resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure
}
}
}
}

View file

@ -10,21 +10,13 @@ import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.discord.panels.PanelsChildGestureRegionObserver
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.IOnBackPressed
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result.result_smallscreen_holder
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreenPlayer(),
PanelsChildGestureRegionObserver.GestureRegionsListener, IOnBackPressed {
open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
override var lockRotation = false
override var isFullScreenPlayer = false
@ -60,13 +52,13 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
screenHeight
}
result_trailer_loading?.isVisible = false
result_smallscreen_holder?.isVisible = !isFullScreenPlayer
result_fullscreen_holder?.isVisible = isFullScreenPlayer
//result_trailer_loading?.isVisible = false
resultBinding?.resultSmallscreenHolder?.isVisible = !isFullScreenPlayer
binding?.resultFullscreenHolder?.isVisible = isFullScreenPlayer
val to = sw * h / w
player_background?.apply {
resultBinding?.fragmentTrailer?.playerBackground?.apply {
isVisible = true
layoutParams =
FrameLayout.LayoutParams(
@ -75,16 +67,17 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
)
}
player_intro_play?.apply {
playerBinding?.playerIntroPlay?.apply {
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
result_top_holder?.measuredHeight ?: FrameLayout.LayoutParams.MATCH_PARENT
resultBinding?.resultTopHolder?.measuredHeight
?: FrameLayout.LayoutParams.MATCH_PARENT
)
}
if (player_intro_play?.isGone == true) {
result_top_holder?.apply {
if (playerBinding?.playerIntroPlay?.isGone == true) {
resultBinding?.resultTopHolder?.apply {
val anim = ValueAnimator.ofInt(
measuredHeight,
@ -131,23 +124,30 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
private fun updateFullscreen(fullscreen: Boolean) {
isFullScreenPlayer = fullscreen
lockRotation = fullscreen
player_fullscreen?.setImageResource(if (fullscreen) R.drawable.baseline_fullscreen_exit_24 else R.drawable.baseline_fullscreen_24)
playerBinding?.playerFullscreen?.setImageResource(if (fullscreen) R.drawable.baseline_fullscreen_exit_24 else R.drawable.baseline_fullscreen_24)
if (fullscreen) {
enterFullscreen()
result_top_bar?.isVisible = false
result_fullscreen_holder?.isVisible = true
result_main_holder?.isVisible = false
player_background?.let { view ->
(view.parent as ViewGroup?)?.removeView(view)
result_fullscreen_holder?.addView(view)
binding?.apply {
resultTopBar.isVisible = false
resultFullscreenHolder.isVisible = true
resultMainHolder.isVisible = false
}
} else {
result_top_bar?.isVisible = true
result_fullscreen_holder?.isVisible = false
result_main_holder?.isVisible = true
player_background?.let { view ->
resultBinding?.fragmentTrailer?.playerBackground?.let { view ->
(view.parent as ViewGroup?)?.removeView(view)
result_smallscreen_holder?.addView(view)
binding?.resultFullscreenHolder?.addView(view)
}
} else {
binding?.apply {
resultTopBar.isVisible = true
resultFullscreenHolder.isVisible = false
resultMainHolder.isVisible = true
resultBinding?.fragmentTrailer?.playerBackground?.let { view ->
(view.parent as ViewGroup?)?.removeView(view)
resultBinding?.resultSmallscreenHolder?.addView(view)
}
}
exitFullscreen()
}
@ -157,14 +157,14 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
player_fullscreen?.setOnClickListener {
playerBinding?.playerFullscreen?.setOnClickListener {
updateFullscreen(!isFullScreenPlayer)
}
updateFullscreen(isFullScreenPlayer)
uiReset()
player_intro_play?.setOnClickListener {
player_intro_play?.isGone = true
playerBinding?.playerIntroPlay?.setOnClickListener {
playerBinding?.playerIntroPlay?.isGone = true
player.handleEvent(CSPlayerEvent.Play)
updateUIVisibility()
fixPlayerSize()

View file

@ -19,6 +19,7 @@ import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.APIHolder.unixTime
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.getCastSession
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
@ -145,15 +146,18 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
minute
)
}
hours > 0 -> txt(
R.string.next_episode_time_hour_format,
hours,
minute
)
minute > 0 -> txt(
R.string.next_episode_time_min_format,
minute
)
else -> null
}?.also {
nextAiringEpisode = txt(R.string.next_episode_format, airing.episode)
@ -305,6 +309,7 @@ fun SelectPopup.getOptions(context: Context): List<String> {
is SelectPopup.SelectArray -> {
this.options.map { it.first.asString(context) }
}
is SelectPopup.SelectText -> options.map { it.asString(context) }
}
}
@ -316,7 +321,7 @@ data class ExtractedTrailerData(
class ResultViewModel2 : ViewModel() {
private var currentResponse: LoadResponse? = null
var EPISODE_RANGE_SIZE: Int = 20
fun clear() {
currentResponse = null
_page.postValue(null)
@ -337,7 +342,7 @@ class ResultViewModel2 : ViewModel() {
private var currentIndex: EpisodeIndexer? = null
private var currentRange: EpisodeRange? = null
private var currentShowFillers: Boolean = false
private var currentRepo: APIRepository? = null
var currentRepo: APIRepository? = null
private var currentId: Int? = null
private var fillers: Map<Int, Boolean> = emptyMap()
private var generator: IGenerator? = null
@ -352,17 +357,17 @@ class ResultViewModel2 : ViewModel() {
MutableLiveData(null)
val page: LiveData<Resource<ResultData>?> = _page
private val _episodes: MutableLiveData<ResourceSome<List<ResultEpisode>>> =
MutableLiveData(ResourceSome.Loading())
val episodes: LiveData<ResourceSome<List<ResultEpisode>>> = _episodes
private val _episodes: MutableLiveData<Resource<List<ResultEpisode>>?> =
MutableLiveData(Resource.Loading())
val episodes: LiveData<Resource<List<ResultEpisode>>?> = _episodes
private val _movie: MutableLiveData<ResourceSome<Pair<UiText, ResultEpisode>>> =
MutableLiveData(ResourceSome.None)
val movie: LiveData<ResourceSome<Pair<UiText, ResultEpisode>>> = _movie
private val _movie: MutableLiveData<Resource<Pair<UiText, ResultEpisode>>?> =
MutableLiveData(null)
val movie: LiveData<Resource<Pair<UiText, ResultEpisode>>?> = _movie
private val _episodesCountText: MutableLiveData<Some<UiText>> =
MutableLiveData(Some.None)
val episodesCountText: LiveData<Some<UiText>> = _episodesCountText
private val _episodesCountText: MutableLiveData<UiText?> =
MutableLiveData(null)
val episodesCountText: LiveData<UiText?> = _episodesCountText
private val _trailers: MutableLiveData<List<ExtractedTrailerData>> =
MutableLiveData(mutableListOf())
@ -384,16 +389,16 @@ class ResultViewModel2 : ViewModel() {
MutableLiveData(emptyList())
val recommendations: LiveData<List<SearchResponse>> = _recommendations
private val _selectedRange: MutableLiveData<Some<UiText>> =
MutableLiveData(Some.None)
val selectedRange: LiveData<Some<UiText>> = _selectedRange
private val _selectedRange: MutableLiveData<UiText?> =
MutableLiveData(null)
val selectedRange: LiveData<UiText?> = _selectedRange
private val _selectedSeason: MutableLiveData<Some<UiText>> =
MutableLiveData(Some.None)
val selectedSeason: LiveData<Some<UiText>> = _selectedSeason
private val _selectedSeason: MutableLiveData<UiText?> =
MutableLiveData(null)
val selectedSeason: LiveData<UiText?> = _selectedSeason
private val _selectedDubStatus: MutableLiveData<Some<UiText>> = MutableLiveData(Some.None)
val selectedDubStatus: LiveData<Some<UiText>> = _selectedDubStatus
private val _selectedDubStatus: MutableLiveData<UiText?> = MutableLiveData(null)
val selectedDubStatus: LiveData<UiText?> = _selectedDubStatus
private val _selectedRangeIndex: MutableLiveData<Int> =
MutableLiveData(-1)
@ -406,12 +411,12 @@ class ResultViewModel2 : ViewModel() {
private val _selectedDubStatusIndex: MutableLiveData<Int> = MutableLiveData(-1)
val selectedDubStatusIndex: LiveData<Int> = _selectedDubStatusIndex
private val _loadedLinks: MutableLiveData<Some<LinkProgress>> = MutableLiveData(Some.None)
val loadedLinks: LiveData<Some<LinkProgress>> = _loadedLinks
private val _loadedLinks: MutableLiveData<LinkProgress?> = MutableLiveData(null)
val loadedLinks: LiveData<LinkProgress?> = _loadedLinks
private val _resumeWatching: MutableLiveData<Some<ResumeWatchingStatus>> =
MutableLiveData(Some.None)
val resumeWatching: LiveData<Some<ResumeWatchingStatus>> = _resumeWatching
private val _resumeWatching: MutableLiveData<ResumeWatchingStatus?> =
MutableLiveData(null)
val resumeWatching: LiveData<ResumeWatchingStatus?> = _resumeWatching
private val _episodeSynopsis: MutableLiveData<String?> = MutableLiveData(null)
val episodeSynopsis: LiveData<String?> = _episodeSynopsis
@ -421,8 +426,8 @@ class ResultViewModel2 : ViewModel() {
companion object {
const val TAG = "RVM2"
private const val EPISODE_RANGE_SIZE = 20
private const val EPISODE_RANGE_OVERLOAD = 30
//private const val EPISODE_RANGE_SIZE = 20
//private const val EPISODE_RANGE_OVERLOAD = 30
private fun List<SeasonData>?.getSeason(season: Int?): SeasonData? {
if (season == null) return null
@ -432,6 +437,8 @@ class ResultViewModel2 : ViewModel() {
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
val currentId = currentResponse.getId()
val currentWatchType = getResultWatchState(currentId)
DataStoreHelper.setResultWatchState(currentId, status.internalId)
val current = DataStoreHelper.getBookmarkedData(currentId)
val currentTime = System.currentTimeMillis()
@ -449,6 +456,9 @@ class ResultViewModel2 : ViewModel() {
currentResponse.year
)
)
if (currentWatchType != status) {
MainActivity.bookmarksUpdatedEvent(true)
}
}
private fun filterName(name: String?): String? {
@ -467,12 +477,16 @@ class ResultViewModel2 : ViewModel() {
)
)
private fun getRanges(allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>): Map<EpisodeIndexer, List<EpisodeRange>> {
private fun getRanges(
allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>,
EPISODE_RANGE_SIZE: Int
): Map<EpisodeIndexer, List<EpisodeRange>> {
return allEpisodes.keys.mapNotNull { index ->
val episodes =
allEpisodes[index] ?: return@mapNotNull null // this should never happened
// fast case
val EPISODE_RANGE_OVERLOAD = EPISODE_RANGE_SIZE + 10
if (episodes.size <= EPISODE_RANGE_OVERLOAD) {
return@mapNotNull index to listOf(
EpisodeRange(
@ -744,7 +758,6 @@ class ResultViewModel2 : ViewModel() {
if (currentLinks.isEmpty()) {
main {
showToast(
activity,
R.string.no_links_found_toast,
Toast.LENGTH_SHORT
)
@ -753,7 +766,6 @@ class ResultViewModel2 : ViewModel() {
} else {
main {
showToast(
activity,
R.string.download_started,
Toast.LENGTH_SHORT
)
@ -800,8 +812,8 @@ class ResultViewModel2 : ViewModel() {
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData(WatchType.NONE)
val watchStatus: LiveData<WatchType> get() = _watchStatus
private val _selectPopup: MutableLiveData<Some<SelectPopup>> = MutableLiveData(Some.None)
val selectPopup: LiveData<Some<SelectPopup>> get() = _selectPopup
private val _selectPopup: MutableLiveData<SelectPopup?> = MutableLiveData(null)
val selectPopup: LiveData<SelectPopup?> = _selectPopup
fun updateWatchStatus(status: WatchType) {
@ -885,23 +897,22 @@ class ResultViewModel2 : ViewModel() {
}
fun cancelLinks() {
println("called::cancelLinks")
currentLoadLinkJob?.cancel()
currentLoadLinkJob = null
_loadedLinks.postValue(Some.None)
_loadedLinks.postValue(null)
}
private fun postPopup(text: UiText, options: List<UiText>, callback: suspend (Int?) -> Unit) {
_selectPopup.postValue(
some(SelectPopup.SelectText(
SelectPopup.SelectText(
text,
options
) { value ->
viewModelScope.launchSafe {
_selectPopup.postValue(Some.None)
_selectPopup.postValue(null)
callback.invoke(value)
}
})
}
)
}
@ -912,15 +923,15 @@ class ResultViewModel2 : ViewModel() {
callback: suspend (Int?) -> Unit
) {
_selectPopup.postValue(
some(SelectPopup.SelectArray(
SelectPopup.SelectArray(
text,
options,
) { value ->
viewModelScope.launchSafe {
_selectPopup.value = Some.None
_selectPopup.postValue(null)
callback.invoke(value)
}
})
}
)
}
@ -988,7 +999,7 @@ class ResultViewModel2 : ViewModel() {
val subs: MutableSet<SubtitleData> = mutableSetOf()
fun updatePage() {
if (isVisible && isActive) {
_loadedLinks.postValue(some(LinkProgress(links.size, subs.size)))
_loadedLinks.postValue(LinkProgress(links.size, subs.size))
}
}
try {
@ -1005,7 +1016,7 @@ class ResultViewModel2 : ViewModel() {
} catch (e: Exception) {
logError(e)
} finally {
_loadedLinks.postValue(Some.None)
_loadedLinks.postValue(null)
}
return LinkLoadingResult(sortUrls(links), sortSubs(subs))
@ -1027,9 +1038,9 @@ class ResultViewModel2 : ViewModel() {
logError(t)
main {
if (t is ActivityNotFoundException) {
showToast(activity, txt(R.string.app_not_found_error), Toast.LENGTH_LONG)
showToast(txt(R.string.app_not_found_error), Toast.LENGTH_LONG)
} else {
showToast(activity, t.toString(), Toast.LENGTH_LONG)
showToast(t.toString(), Toast.LENGTH_LONG)
}
}
}
@ -1138,9 +1149,9 @@ class ResultViewModel2 : ViewModel() {
}
fun handleAction(activity: Activity?, click: EpisodeClickEvent) =
fun handleAction(click: EpisodeClickEvent) =
viewModelScope.launchSafe {
handleEpisodeClickEvent(activity, click)
handleEpisodeClickEvent(click)
}
data class ExternalApp(
@ -1170,7 +1181,7 @@ class ResultViewModel2 : ViewModel() {
_episodeSynopsis.postValue(null)
}
private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) {
private suspend fun handleEpisodeClickEvent(click: EpisodeClickEvent) {
when (click.action) {
ACTION_SHOW_OPTIONS -> {
val options = mutableListOf<Pair<UiText, Int>>()
@ -1228,27 +1239,26 @@ class ResultViewModel2 : ViewModel() {
options
) { result ->
handleEpisodeClickEvent(
activity,
click.copy(action = result ?: return@postPopup)
)
}
}
ACTION_CLICK_DEFAULT -> {
activity?.let { ctx ->
if (ctx.isConnectedToChromecast()) {
handleEpisodeClickEvent(
activity,
click.copy(action = ACTION_CHROME_CAST_EPISODE)
)
} else {
val action = getPlayerAction(ctx)
handleEpisodeClickEvent(
activity,
click.copy(action = action)
)
}
}
}
ACTION_SHOW_DESCRIPTION -> {
_episodeSynopsis.postValue(click.data.description)
}
@ -1280,15 +1290,16 @@ class ResultViewModel2 : ViewModel() {
)
)
showToast(
activity,
R.string.download_started,
Toast.LENGTH_SHORT
)
}
}
ACTION_SHOW_TOAST -> {
showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT)
showToast(R.string.play_episode_toast, Toast.LENGTH_SHORT)
}
ACTION_DOWNLOAD_EPISODE -> {
val response = currentResponse ?: return
downloadEpisode(
@ -1303,6 +1314,7 @@ class ResultViewModel2 : ViewModel() {
response.url
)
}
ACTION_DOWNLOAD_MIRROR -> {
val response = currentResponse ?: return
acquireSingleLink(
@ -1326,12 +1338,12 @@ class ResultViewModel2 : ViewModel() {
)
}
showToast(
activity,
R.string.download_started,
Toast.LENGTH_SHORT
)
}
}
ACTION_RELOAD_EPISODE -> {
ioSafe {
loadLinks(
@ -1342,6 +1354,7 @@ class ResultViewModel2 : ViewModel() {
)
}
}
ACTION_CHROME_CAST_MIRROR -> {
acquireSingleLink(
click.data,
@ -1351,6 +1364,7 @@ class ResultViewModel2 : ViewModel() {
startChromecast(activity, click.data, result.links, result.subs, index)
}
}
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
click.data,
isCasting = true,
@ -1364,6 +1378,7 @@ class ResultViewModel2 : ViewModel() {
logError(e)
}
}
ACTION_COPY_LINK -> {
acquireSingleLink(
click.data,
@ -1377,16 +1392,18 @@ class ResultViewModel2 : ViewModel() {
val link = result.links[index]
val clip = ClipData.newPlainText(link.name, link.url)
serviceClipboard.setPrimaryClip(clip)
showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT)
showToast(R.string.copy_link_toast, Toast.LENGTH_SHORT)
}
}
ACTION_CHROME_CAST_EPISODE -> {
startChromecast(activity, click.data)
}
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
loadLinks(click.data, isVisible = true, isCasting = true) { links ->
if (links.links.isEmpty()) {
showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
return@loadLinks
}
@ -1397,6 +1414,7 @@ class ResultViewModel2 : ViewModel() {
)
}
}
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
click.data,
isCasting = true,
@ -1413,6 +1431,7 @@ class ResultViewModel2 : ViewModel() {
result.subs
)
}
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
click.data,
isCasting = true,
@ -1428,6 +1447,7 @@ class ResultViewModel2 : ViewModel() {
result.subs
)
}
ACTION_PLAY_EPISODE_IN_PLAYER -> {
val data = currentResponse?.syncData?.toList() ?: emptyList()
val list =
@ -1448,6 +1468,7 @@ class ResultViewModel2 : ViewModel() {
)
)
}
ACTION_MARK_AS_WATCHED -> {
val isWatched =
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
@ -1487,13 +1508,14 @@ class ResultViewModel2 : ViewModel() {
}
val realRecommendations = ArrayList<SearchResponse>()
val apiNames = apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
val apiNames = synchronized(apis) {
apis.filter {
it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true)
}.map {
it.name
}
}
meta.recommendations?.forEach { rec ->
apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name))
@ -1672,10 +1694,10 @@ class ResultViewModel2 : ViewModel() {
private fun postMovie() {
val response = currentResponse
_episodes.postValue(ResourceSome.None)
_episodes.postValue(null)
if (response == null) {
_movie.postValue(ResourceSome.None)
_movie.postValue(null)
return
}
@ -1692,11 +1714,11 @@ class ResultViewModel2 : ViewModel() {
}
)
val data = getMovie()
_episodes.postValue(ResourceSome.None)
_episodes.postValue(null)
if (text == null || data == null) {
_movie.postValue(ResourceSome.None)
_movie.postValue(null)
} else {
_movie.postValue(ResourceSome.Success(text to data))
_movie.postValue(Resource.Success(text to data))
}
}
@ -1705,14 +1727,14 @@ class ResultViewModel2 : ViewModel() {
postMovie()
} else {
_episodes.postValue(
ResourceSome.Success(
Resource.Success(
getEpisodes(
currentIndex ?: return,
currentRange ?: return
)
)
)
_movie.postValue(ResourceSome.None)
_movie.postValue(null)
}
postResume()
}
@ -1755,14 +1777,14 @@ class ResultViewModel2 : ViewModel() {
val size = currentEpisodes[indexer]?.size
_episodesCountText.postValue(
some(
if (isMovie) null else
txt(
R.string.episode_format,
size,
txt(if (size == 1) R.string.episode else R.string.episodes),
)
)
if (isMovie) null else
txt(
R.string.episode_format,
size,
txt(if (size == 1) R.string.episode else R.string.episodes),
)
)
_selectedSeasonIndex.postValue(
@ -1770,29 +1792,29 @@ class ResultViewModel2 : ViewModel() {
)
_selectedSeason.postValue(
some(
if (isMovie || currentSeasons.size <= 1) null else
when (indexer.season) {
0 -> txt(R.string.no_season)
else -> {
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
val seasonData = seasonNames.getSeason(indexer.season)
// If displaySeason is null then only show the name!
if (seasonData?.name != null && seasonData.displaySeason == null) {
txt(seasonData.name)
} else {
val suffix = seasonData?.name?.let { " $it" } ?: ""
txt(
R.string.season_format,
txt(R.string.season),
seasonData?.displaySeason ?: indexer.season,
suffix
)
}
if (isMovie || currentSeasons.size <= 1) null else
when (indexer.season) {
0 -> txt(R.string.no_season)
else -> {
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
val seasonData = seasonNames.getSeason(indexer.season)
// If displaySeason is null then only show the name!
if (seasonData?.name != null && seasonData.displaySeason == null) {
txt(seasonData.name)
} else {
val suffix = seasonData?.name?.let { " $it" } ?: ""
txt(
R.string.season_format,
txt(R.string.season),
seasonData?.displaySeason ?: indexer.season,
suffix
)
}
}
)
}
)
_selectedRangeIndex.postValue(
@ -1800,13 +1822,13 @@ class ResultViewModel2 : ViewModel() {
)
_selectedRange.postValue(
some(
if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) {
txt(R.string.episodes_range, range.startEpisode, range.endEpisode)
} else {
null
}
)
if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) {
txt(R.string.episodes_range, range.startEpisode, range.endEpisode)
} else {
null
}
)
_selectedDubStatusIndex.postValue(
@ -1814,10 +1836,10 @@ class ResultViewModel2 : ViewModel() {
)
_selectedDubStatus.postValue(
some(
if (isMovie || currentDubStatus.size <= 1) null else
txt(indexer.dubStatus)
)
if (isMovie || currentDubStatus.size <= 1) null else
txt(indexer.dubStatus)
)
currentId?.let { id ->
@ -1851,7 +1873,7 @@ class ResultViewModel2 : ViewModel() {
}
}*/
_episodes.postValue(ResourceSome.Success(ret))
_episodes.postValue(Resource.Success(ret))
}
}
@ -1869,7 +1891,7 @@ class ResultViewModel2 : ViewModel() {
}
private suspend fun postEpisodes(loadResponse: LoadResponse, updateFillers: Boolean) {
_episodes.postValue(ResourceSome.Loading())
_episodes.postValue(Resource.Loading())
val mainId = loadResponse.getId()
currentId = mainId
@ -1924,6 +1946,7 @@ class ResultViewModel2 : ViewModel() {
}
episodes
}
is TvSeriesLoadResponse -> {
val episodes: MutableMap<EpisodeIndexer, MutableList<ResultEpisode>> =
mutableMapOf()
@ -1968,6 +1991,7 @@ class ResultViewModel2 : ViewModel() {
}
episodes
}
is MovieLoadResponse -> {
singleMap(
buildResultEpisode(
@ -1989,6 +2013,7 @@ class ResultViewModel2 : ViewModel() {
)
)
}
is LiveStreamLoadResponse -> {
singleMap(
buildResultEpisode(
@ -2010,6 +2035,7 @@ class ResultViewModel2 : ViewModel() {
)
)
}
is TorrentLoadResponse -> {
singleMap(
buildResultEpisode(
@ -2031,6 +2057,7 @@ class ResultViewModel2 : ViewModel() {
)
)
}
else -> {
mapOf()
}
@ -2066,7 +2093,7 @@ class ResultViewModel2 : ViewModel() {
}
currentEpisodes = allEpisodes
val ranges = getRanges(allEpisodes)
val ranges = getRanges(allEpisodes, EPISODE_RANGE_SIZE)
currentRanges = ranges
@ -2088,7 +2115,7 @@ class ResultViewModel2 : ViewModel() {
}
fun postResume() {
_resumeWatching.postValue(some(resume()))
_resumeWatching.postValue(resume())
}
private fun resume(): ResumeWatchingStatus? {
@ -2186,7 +2213,6 @@ class ResultViewModel2 : ViewModel() {
for (ep in currentRange) {
if (ep.getWatchProgress() > 0.9) continue
handleAction(
activity,
EpisodeClickEvent(
getPlayerAction(activity),
ep
@ -2196,6 +2222,7 @@ class ResultViewModel2 : ViewModel() {
}
}
}
START_ACTION_LOAD_EP -> {
val all = currentEpisodes.values.flatten()
val episode =
@ -2206,7 +2233,6 @@ class ResultViewModel2 : ViewModel() {
}
?: return@launchSafe
handleAction(
activity,
EpisodeClickEvent(
getPlayerAction(activity),
episode
@ -2227,7 +2253,7 @@ class ResultViewModel2 : ViewModel() {
) =
ioSafe {
_page.postValue(Resource.Loading(url))
_episodes.postValue(ResourceSome.Loading())
_episodes.postValue(Resource.Loading())
preferDubStatus = dubStatus
currentShowFillers = showFillers
@ -2271,6 +2297,7 @@ class ResultViewModel2 : ViewModel() {
is Resource.Failure -> {
_page.postValue(data)
}
is Resource.Success -> {
if (!isActive) return@ioSafe
val loadResponse = ioWork {
@ -2307,6 +2334,7 @@ class ResultViewModel2 : ViewModel() {
if (!isActive) return@ioSafe
handleAutoStart(activity, autostart)
}
is Resource.Loading -> {
debugException { "Invalid load result" }
}

View file

@ -1,12 +1,11 @@
package com.lagradost.cloudstream3.ui.result
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ResultSelectionBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
typealias SelectData = Pair<UiText?, Any>
@ -17,7 +16,9 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return SelectViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.result_selection, parent, false),
ResultSelectionBinding.inflate(LayoutInflater.from(parent.context), parent, false),
//LayoutInflater.from(parent.context).inflate(R.layout.result_selection, parent, false),
)
}
@ -73,10 +74,10 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
private class SelectViewHolder
constructor(
itemView: View,
binding: ResultSelectionBinding,
) :
RecyclerView.ViewHolder(itemView) {
private val item: MaterialButton = itemView as MaterialButton
RecyclerView.ViewHolder(binding.root) {
private val item: MaterialButton = binding.root
fun update(isSelected: Boolean) {
item.isSelected = isSelected

View file

@ -8,7 +8,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -162,11 +161,3 @@ fun TextView?.setTextHtml(text: UiText?) {
this.text = str.html()
}
}
fun TextView?.setTextHtml(text: Some<UiText>?) {
setTextHtml(if (text is Some.Success) text.value else null)
}
fun TextView?.setText(text: Some<UiText>?) {
setText(if (text is Some.Success) text.value else null)
}

View file

@ -4,16 +4,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.SearchResultGridBinding
import com.lagradost.cloudstream3.databinding.SearchResultGridExpandedBinding
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.search_result_compact.view.*
import kotlin.math.roundToInt
/** Click */
@ -39,10 +38,23 @@ class SearchAdapter(
var hasNext: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val layout =
if (parent.context.IsBottomLayout()) R.layout.search_result_grid_expanded else R.layout.search_result_grid
if (parent.context.IsBottomLayout()) SearchResultGridExpandedBinding.inflate(
inflater,
parent,
false
) else SearchResultGridBinding.inflate(
inflater,
parent,
false
) //R.layout.search_result_grid_expanded else R.layout.search_result_grid
return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false),
layout,
clickCallback,
resView
)
@ -73,20 +85,25 @@ class SearchAdapter(
class CardViewHolder
constructor(
itemView: View,
val binding: ViewBinding,
private val clickCallback: (SearchClickCallback) -> Unit,
resView: AutofitRecyclerView
) :
RecyclerView.ViewHolder(itemView) {
val cardView: ImageView = itemView.imageView
RecyclerView.ViewHolder(binding.root) {
private val compactView = false//itemView.context.getGridIsCompact()
private val coverHeight: Int =
if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt()
private val cardView = when(binding) {
is SearchResultGridExpandedBinding -> binding.imageView
is SearchResultGridBinding -> binding.imageView
else -> null
}
fun bind(card: SearchResponse, position: Int) {
if (!compactView) {
cardView.apply {
cardView?.apply {
layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
coverHeight

View file

@ -33,6 +33,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
@ -56,8 +58,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.tvtypes_chips.*
import java.util.concurrent.locks.ReentrantLock
const val SEARCH_PREF_TAGS = "search_pref_tags"
@ -89,6 +89,7 @@ class SearchFragment : Fragment() {
private val searchViewModel: SearchViewModel by activityViewModels()
private var bottomSheetDialog: BottomSheetDialog? = null
var binding: FragmentSearchBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
@ -99,18 +100,21 @@ class SearchFragment : Fragment() {
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
)
bottomSheetDialog?.ownShow()
return inflater.inflate(
if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search,
container,
false
)
val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search
val root = inflater.inflate(layout, container, false)
// TODO TRYCATCH
binding = FragmentSearchBinding.bind(root)
return root
}
private fun fixGrid() {
activity?.getSpanCount()?.let {
currentSpan = it
}
search_autofit_results.spanCount = currentSpan
binding?.searchAutofitResults?.spanCount = currentSpan
currentSpan = currentSpan
HomeFragment.configEvent.invoke(currentSpan)
}
@ -123,6 +127,7 @@ class SearchFragment : Fragment() {
override fun onDestroyView() {
hideKeyboard()
bottomSheetDialog?.ownHide()
binding = null
super.onDestroyView()
}
@ -181,7 +186,7 @@ class SearchFragment : Fragment() {
searchViewModel.reloadRepos()
context?.filterProviderByPreferredMedia()?.let { validAPIs ->
bindChips(
home_select_group,
binding?.tvtypesChipsScroll?.tvtypesChips,
selectedSearchTypes,
validAPIs.flatMap { api -> api.supportedTypes }.distinct()
) { list ->
@ -189,7 +194,7 @@ class SearchFragment : Fragment() {
setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
selectedSearchTypes.clear()
selectedSearchTypes.addAll(list)
search(main_search?.query?.toString())
search(binding?.mainSearch?.query?.toString())
}
}
}
@ -199,24 +204,27 @@ class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(searchRoot)
fixPaddingStatusbar(binding?.searchRoot)
fixGrid()
reloadRepos()
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let {
SearchAdapter(
ArrayList(),
search_autofit_results,
) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
}
binding?.apply {
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? =
SearchAdapter(
ArrayList(),
searchAutofitResults,
) { callback ->
SearchHelper.handleSearchClickCallback(callback)
}
searchAutofitResults.adapter = adapter
searchLoadingBar.alpha = 0f
}
search_autofit_results.adapter = adapter
search_loading_bar.alpha = 0f
val searchExitIcon =
main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
// val searchMagIcon =
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
//searchMagIcon.scaleX = 0.65f
@ -230,7 +238,7 @@ class SearchFragment : Fragment() {
)!!.toMutableSet()
}
search_filter.setOnClickListener { searchView ->
binding?.searchFilter?.setOnClickListener { searchView ->
searchView?.context?.let { ctx ->
val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
var currentValidApis = listOf<MainAPI>()
@ -241,7 +249,13 @@ class SearchFragment : Fragment() {
BottomSheetDialog(ctx)
builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED
builder.setContentView(R.layout.home_select_mainpage)
val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate(
builder.layoutInflater,
null,
false
)
builder.setContentView(binding.root)
builder.show()
builder.let { dialog ->
val isMultiLang = ctx.getApiProviderLangSettings().let { set ->
@ -303,7 +317,7 @@ class SearchFragment : Fragment() {
?: mutableListOf(TvType.Movie, TvType.TvSeries)
bindChips(
dialog.home_select_group,
binding.tvtypesChipsScroll.tvtypesChips,
selectedSearchTypes,
TvType.values().toList()
) { list ->
@ -343,15 +357,15 @@ class SearchFragment : Fragment() {
?: mutableListOf(TvType.Movie, TvType.TvSeries)
if (isTrueTvSettings()) {
search_filter.isFocusable = true
search_filter.isFocusableInTouchMode = true
binding?.searchFilter?.isFocusable = true
binding?.searchFilter?.isFocusableInTouchMode = true
}
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
search(query)
main_search?.let {
binding?.mainSearch?.let {
hideKeyboard(it)
}
@ -365,17 +379,17 @@ class SearchFragment : Fragment() {
searchViewModel.clearSearch()
searchViewModel.updateHistory()
}
search_history_holder?.isVisible = showHistory
search_master_recycler?.isVisible = !showHistory && isAdvancedSearch
search_autofit_results?.isVisible = !showHistory && !isAdvancedSearch
binding?.apply {
searchHistoryHolder.isVisible = showHistory
searchMasterRecycler.isVisible = !showHistory && isAdvancedSearch
searchAutofitResults.isVisible = !showHistory && !isAdvancedSearch
}
return true
}
})
search_clear_call_history?.setOnClickListener {
binding?.searchClearCallHistory?.setOnClickListener {
activity?.let { ctx ->
val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
val dialogClickListener =
@ -409,8 +423,8 @@ class SearchFragment : Fragment() {
}
observe(searchViewModel.currentHistory) { list ->
search_clear_call_history?.isVisible = list.isNotEmpty()
(search_history_recycler.adapter as? SearchHistoryAdaptor?)?.updateList(list)
binding?.searchClearCallHistory?.isVisible = list.isNotEmpty()
(binding?.searchHistoryRecycler?.adapter as? SearchHistoryAdaptor?)?.updateList(list)
}
searchViewModel.updateHistory()
@ -420,20 +434,20 @@ class SearchFragment : Fragment() {
is Resource.Success -> {
it.value.let { data ->
if (data.isNotEmpty()) {
(search_autofit_results?.adapter as? SearchAdapter)?.updateList(data)
(binding?.searchAutofitResults?.adapter as? SearchAdapter)?.updateList(data)
}
}
searchExitIcon.alpha = 1f
search_loading_bar.alpha = 0f
searchExitIcon?.alpha = 1f
binding?.searchLoadingBar?.alpha = 0f
}
is Resource.Failure -> {
// Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show()
searchExitIcon.alpha = 1f
search_loading_bar.alpha = 0f
searchExitIcon?.alpha = 1f
binding?.searchLoadingBar?.alpha = 0f
}
is Resource.Loading -> {
searchExitIcon.alpha = 0f
search_loading_bar.alpha = 1f
searchExitIcon?.alpha = 0f
binding?.searchLoadingBar?.alpha = 1f
}
}
}
@ -443,7 +457,7 @@ class SearchFragment : Fragment() {
try {
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
listLock.lock()
(search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
(binding?.searchMasterRecycler?.adapter as ParentItemAdapter?)?.apply {
val newItems = list.map { ongoing ->
val dataList =
if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList()
@ -477,7 +491,7 @@ class SearchFragment : Fragment() {
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
ParentItemAdapter(mutableListOf(), { callback ->
SearchHelper.handleSearchClickCallback(activity, callback)
SearchHelper.handleSearchClickCallback(callback)
}, { item ->
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
bottomSheetDialog = null
@ -490,8 +504,8 @@ class SearchFragment : Fragment() {
SEARCH_HISTORY_OPEN -> {
searchViewModel.clearSearch()
if (searchItem.type.isNotEmpty())
updateChips(home_select_group, searchItem.type.toMutableList())
main_search?.setQuery(searchItem.searchText, true)
updateChips(binding?.tvtypesChipsScroll?.tvtypesChips, searchItem.type.toMutableList())
binding?.mainSearch?.setQuery(searchItem.searchText, true)
}
SEARCH_HISTORY_REMOVE -> {
removeKey(SEARCH_HISTORY_KEY, searchItem.key)
@ -503,20 +517,23 @@ class SearchFragment : Fragment() {
}
}
search_history_recycler?.adapter = historyAdapter
search_history_recycler?.layoutManager = GridLayoutManager(context, 1)
binding?.apply {
searchHistoryRecycler.adapter = historyAdapter
searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1)
search_master_recycler?.adapter = masterAdapter
search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
searchMasterRecycler.adapter = masterAdapter
searchMasterRecycler.layoutManager = GridLayoutManager(context, 1)
// Automatically search the specified query, this allows the app search to launch from intent
arguments?.getString(SEARCH_QUERY)?.let { query ->
if (query.isBlank()) return@let
main_search?.setQuery(query, true)
// Clear the query as to not make it request the same query every time the page is opened
arguments?.putString(SEARCH_QUERY, null)
// Automatically search the specified query, this allows the app search to launch from intent
arguments?.getString(SEARCH_QUERY)?.let { query ->
if (query.isBlank()) return@let
mainSearch.setQuery(query, true)
// Clear the query as to not make it request the same query every time the page is opened
arguments?.putString(SEARCH_QUERY, null)
}
}
// SubtitlesFragment.push(activity)
//searchViewModel.search("iron man")
//(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro")

View file

@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
@ -15,21 +16,21 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
object SearchHelper {
fun handleSearchClickCallback(activity: Activity?, callback: SearchClickCallback) {
fun handleSearchClickCallback(callback: SearchClickCallback) {
val card = callback.card
when (callback.action) {
SEARCH_ACTION_LOAD -> {
activity.loadSearchResult(card)
loadSearchResult(card)
}
SEARCH_ACTION_PLAY_FILE -> {
if (card is DataStoreHelper.ResumeWatchingResult) {
val id = card.id
if(id == null) {
showToast(activity, R.string.error_invalid_id, Toast.LENGTH_SHORT)
showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT)
} else {
if (card.isFromDownload) {
handleDownloadClick(
activity, DownloadClickEvent(
DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached(
card.name,
@ -45,12 +46,11 @@ object SearchHelper {
)
)
} else {
activity.loadSearchResult(card, START_ACTION_LOAD_EP, id)
loadSearchResult(card, START_ACTION_LOAD_EP, id)
}
}
} else {
handleSearchClickCallback(
activity,
SearchClickCallback(SEARCH_ACTION_LOAD, callback.view, -1, callback.card)
)
}
@ -60,10 +60,10 @@ object SearchHelper {
(activity as? MainActivity?)?.apply {
loadPopup(callback.card)
} ?: kotlin.run {
showToast(activity, callback.card.name, Toast.LENGTH_SHORT)
showToast(callback.card.name, Toast.LENGTH_SHORT)
}
} else {
showToast(activity, callback.card.name, Toast.LENGTH_SHORT)
showToast(callback.card.name, Toast.LENGTH_SHORT)
}
}
}

View file

@ -10,7 +10,8 @@ import androidx.recyclerview.widget.RecyclerView
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import kotlinx.android.synthetic.main.search_history_item.view.*
import com.lagradost.cloudstream3.databinding.AccountSingleBinding
import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding
data class SearchHistoryItem(
@JsonProperty("searchedAt") val searchedAt: Long,
@ -34,8 +35,7 @@ class SearchHistoryAdaptor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.search_history_item, parent, false),
SearchHistoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
clickCallback,
)
}
@ -65,22 +65,24 @@ class SearchHistoryAdaptor(
class CardViewHolder
constructor(
itemView: View,
val binding: SearchHistoryItemBinding,
private val clickCallback: (SearchHistoryCallback) -> Unit,
) :
RecyclerView.ViewHolder(itemView) {
private val removeButton: ImageView = itemView.home_history_remove
private val openButton: View = itemView.home_history_tab
private val title: TextView = itemView.home_history_title
RecyclerView.ViewHolder(binding.root) {
// private val removeButton: ImageView = itemView.home_history_remove
// private val openButton: View = itemView.home_history_tab
// private val title: TextView = itemView.home_history_title
fun bind(card: SearchHistoryItem) {
title.text = card.searchText
binding.apply {
homeHistoryTitle.text = card.searchText
removeButton.setOnClickListener {
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE))
}
openButton.setOnClickListener {
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN))
homeHistoryRemove.setOnClickListener {
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE))
}
homeHistoryTab.setOnClickListener {
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN))
}
}
}
}

View file

@ -1,7 +1,6 @@
package com.lagradost.cloudstream3.ui.search
import android.content.Context
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import android.widget.ProgressBar
@ -10,14 +9,20 @@ import androidx.cardview.widget.CardView
import androidx.core.view.isVisible
import androidx.palette.graphics.Palette
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.AnimeSearchResponse
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LiveSearchResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.isMovieType
import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.home_result_grid.view.*
object SearchResultBuilder {
private val showCache: MutableMap<String, Boolean> = mutableMapOf()
@ -45,19 +50,21 @@ object SearchResultBuilder {
nextFocusDown: Int? = null,
colorCallback : ((Palette) -> Unit)? = null
) {
val cardView: ImageView = itemView.imageView
val cardText: TextView? = itemView.imageText
val cardView: ImageView = itemView.findViewById(R.id.imageView)
val cardText: TextView? = itemView.findViewById(R.id.imageText)
val textIsDub: TextView? = itemView.text_is_dub
val textIsSub: TextView? = itemView.text_is_sub
val textFlag: TextView? = itemView.text_flag
val textQuality: TextView? = itemView.text_quality
val shadow: View? = itemView.title_shadow
val textIsDub: TextView? = itemView.findViewById(R.id.text_is_dub)
val textIsSub: TextView? = itemView.findViewById(R.id.text_is_sub)
val textFlag: TextView? = itemView.findViewById(R.id.text_flag)
val rating: TextView? = itemView.findViewById(R.id.text_rating)
val bg: CardView = itemView.background_card
val textQuality: TextView? = itemView.findViewById(R.id.text_quality)
val shadow: View? = itemView.findViewById(R.id.title_shadow)
val bar: ProgressBar? = itemView.watchProgress
val playImg: ImageView? = itemView.search_item_download_play
val bg: CardView = itemView.findViewById(R.id.background_card)
val bar: ProgressBar? = itemView.findViewById(R.id.watchProgress)
val playImg: ImageView? = itemView.findViewById(R.id.search_item_download_play)
// Do logic
@ -66,12 +73,25 @@ object SearchResultBuilder {
textIsDub?.isVisible = false
textIsSub?.isVisible = false
textFlag?.isVisible = false
rating?.isVisible = false
val showSub = showCache[textIsDub?.context?.getString(R.string.show_sub_key)] ?: false
val showDub = showCache[textIsDub?.context?.getString(R.string.show_dub_key)] ?: false
val showTitle = showCache[cardText?.context?.getString(R.string.show_title_key)] ?: false
val showHd = showCache[textQuality?.context?.getString(R.string.show_hd_key)] ?: false
if(card is SyncAPI.LibraryItem) {
val showRating = (card.personalRating ?: 0) != 0
rating?.isVisible = showRating
if (showRating) {
// We want to show 8.5 but not 8.0 hence the replace
val ratingText = ((card.personalRating ?: 0).toDouble() / 10).toString()
.replace(".0", "")
rating?.text = ratingText
}
}
shadow?.isVisible = showTitle
when (card.quality) {
@ -142,15 +162,42 @@ object SearchResultBuilder {
}
}
bg.setOnClickListener {
click(it)
bg.isFocusable = false
bg.isFocusableInTouchMode = false
if(!isTrueTvSettings()) {
bg.setOnClickListener {
click(it)
}
bg.setOnLongClickListener {
longClick(it)
return@setOnLongClickListener true
}
}
//
//
//
itemView.setOnClickListener {
click(it)
}
if (nextFocusUp != null) {
itemView.nextFocusUpId = nextFocusUp
}
if (nextFocusDown != null) {
itemView.nextFocusDownId = nextFocusDown
}
/*when (nextFocusBehavior) {
true -> itemView.nextFocusLeftId = bg.id
false -> itemView.nextFocusRightId = bg.id
null -> {
bg.nextFocusRightId = -1
bg.nextFocusLeftId = -1
}
}*/
/*if (nextFocusUp != null) {
bg.nextFocusUpId = nextFocusUp
}
@ -158,36 +205,26 @@ object SearchResultBuilder {
bg.nextFocusDownId = nextFocusDown
}
when (nextFocusBehavior) {
true -> bg.nextFocusLeftId = bg.id
false -> bg.nextFocusRightId = bg.id
null -> {
bg.nextFocusRightId = -1
bg.nextFocusLeftId = -1
}
}
*/
if (isTrueTvSettings()) {
bg.isFocusable = true
bg.isFocusableInTouchMode = true
bg.touchscreenBlocksFocus = false
// bg.isFocusable = true
// bg.isFocusableInTouchMode = true
// bg.touchscreenBlocksFocus = false
itemView.isFocusableInTouchMode = true
itemView.isFocusable = true
}
bg.setOnLongClickListener {
longClick(it)
return@setOnLongClickListener true
}
/**/
itemView.setOnLongClickListener {
longClick(it)
return@setOnLongClickListener true
}
bg.setOnFocusChangeListener { view, b ->
/*bg.setOnFocusChangeListener { view, b ->
focus(view, b)
}
}*/
itemView.setOnFocusChangeListener { view, b ->
focus(view, b)

View file

@ -37,7 +37,7 @@ class SearchViewModel : ViewModel() {
private val _currentHistory: MutableLiveData<List<SearchHistoryItem>> = MutableLiveData()
val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory
private var repos = apis.map { APIRepository(it) }
private var repos = synchronized(apis) { apis.map { APIRepository(it) } }
fun clearSearch() {
_searchResponse.postValue(Resource.Success(ArrayList()))
@ -48,7 +48,7 @@ class SearchViewModel : ViewModel() {
private var onGoingSearch: Job? = null
fun reloadRepos() {
repos = apis.map { APIRepository(it) }
repos = synchronized(apis) { apis.map { APIRepository(it) } }
}
fun searchAndCancel(

View file

@ -3,11 +3,10 @@ package com.lagradost.cloudstream3.ui.settings
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountSingleBinding
import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -15,14 +14,15 @@ class AccountClickCallback(val action: Int, val view: View, val card: AuthAPI.Lo
class AccountAdapter(
val cardList: List<AuthAPI.LoginInfo>,
val layout: Int = R.layout.account_single,
private val clickCallback: (AccountClickCallback) -> Unit
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false), clickCallback
AccountSingleBinding.inflate(LayoutInflater.from(parent.context), parent, false), //LayoutInflater.from(parent.context).inflate(layout, parent, false),
clickCallback
)
}
@ -43,18 +43,18 @@ class AccountAdapter(
}
class CardViewHolder
constructor(itemView: View, private val clickCallback: (AccountClickCallback) -> Unit) :
RecyclerView.ViewHolder(itemView) {
private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) :
RecyclerView.ViewHolder(binding.root) {
// private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
// private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
fun bind(card: AuthAPI.LoginInfo) {
// just in case name is null account index will show, should never happened
accountName.text = card.name ?: "%s %d".format(
accountName.context.getString(R.string.account),
binding.accountName.text = card.name ?: "%s %d".format(
binding.accountName.context.getString(R.string.account),
card.accountIndex
)
pfp.isVisible = pfp.setImage(card.profilePicture)
binding.accountProfilePicture.isVisible = binding.accountProfilePicture.setImage(card.profilePicture)
itemView.setOnClickListener {
clickCallback.invoke(AccountClickCallback(0, itemView, card))

View file

@ -15,6 +15,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountManagmentBinding
import com.lagradost.cloudstream3.databinding.AccountSwitchBinding
import com.lagradost.cloudstream3.databinding.AddAccountInputBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi
@ -31,9 +34,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.account_managment.*
import kotlinx.android.synthetic.main.account_switch.*
import kotlinx.android.synthetic.main.add_account_input.*
class SettingsAccount : PreferenceFragmentCompat() {
companion object {
@ -43,15 +43,18 @@ class SettingsAccount : PreferenceFragmentCompat() {
api: AccountManager,
info: AuthAPI.LoginInfo
) {
if (activity == null) return
val binding: AccountManagmentBinding =
AccountManagmentBinding.inflate(activity.layoutInflater, null, false)
val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom)
.setView(R.layout.account_managment)
AlertDialog.Builder(activity, R.style.AlertDialogCustom)
.setView(binding.root)
val dialog = builder.show()
dialog.account_main_profile_picture_holder?.isVisible =
dialog.account_main_profile_picture?.setImage(info.profilePicture) == true
binding.accountMainProfilePictureHolder.isVisible =
binding.accountMainProfilePicture.setImage(info.profilePicture)
dialog.account_logout?.setOnClickListener {
binding.accountLogout.setOnClickListener {
api.logOut()
dialog.dismissSafe(activity)
}
@ -60,26 +63,28 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.findViewById<TextView>(R.id.account_name)?.text = it
}
dialog.account_site?.text = api.name
dialog.account_switch_account?.setOnClickListener {
binding.accountSite.text = api.name
binding.accountSwitchAccount.setOnClickListener {
dialog.dismissSafe(activity)
showAccountSwitch(activity, api)
}
if (isTvSettings()) {
dialog.account_switch_account?.requestFocus()
binding.accountSwitchAccount.requestFocus()
}
}
fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) {
private fun showAccountSwitch(activity: FragmentActivity, api: AccountManager) {
val accounts = api.getAccounts() ?: return
val binding: AccountSwitchBinding =
AccountSwitchBinding.inflate(activity.layoutInflater, null, false)
val builder =
AlertDialog.Builder(activity, R.style.AlertDialogCustom)
.setView(R.layout.account_switch)
.setView(binding.root)
val dialog = builder.show()
dialog.account_add?.setOnClickListener {
binding.accountAdd.setOnClickListener {
addAccount(activity, api)
dialog?.dismissSafe(activity)
}
@ -96,7 +101,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
}
}
api.accountIndex = ogIndex
val adapter = AccountAdapter(items, R.layout.account_single) {
val adapter = AccountAdapter(items) {
dialog?.dismissSafe(activity)
api.changeAccount(it.card.accountIndex)
}
@ -111,17 +116,21 @@ class SettingsAccount : PreferenceFragmentCompat() {
is OAuth2API -> {
api.authenticate(activity)
}
is InAppAuthAPI -> {
if (activity == null) return
val binding: AddAccountInputBinding =
AddAccountInputBinding.inflate(activity.layoutInflater, null, false)
val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom)
.setView(R.layout.add_account_input)
AlertDialog.Builder(activity, R.style.AlertDialogCustom)
.setView(binding.root)
val dialog = builder.show()
val visibilityMap = mapOf(
dialog.login_email_input to api.requiresEmail,
dialog.login_password_input to api.requiresPassword,
dialog.login_server_input to api.requiresServer,
dialog.login_username_input to api.requiresUsername
val visibilityMap = listOf(
binding.loginEmailInput to api.requiresEmail,
binding.loginPasswordInput to api.requiresPassword,
binding.loginServerInput to api.requiresServer,
binding.loginUsernameInput to api.requiresUsername
)
if (isTvSettings()) {
@ -145,12 +154,12 @@ class SettingsAccount : PreferenceFragmentCompat() {
}
}
dialog.login_email_input?.isVisible = api.requiresEmail
dialog.login_password_input?.isVisible = api.requiresPassword
dialog.login_server_input?.isVisible = api.requiresServer
dialog.login_username_input?.isVisible = api.requiresUsername
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank()
dialog.create_account?.setOnClickListener {
binding.loginEmailInput.isVisible = api.requiresEmail
binding.loginPasswordInput.isVisible = api.requiresPassword
binding.loginServerInput.isVisible = api.requiresServer
binding.loginUsernameInput.isVisible = api.requiresUsername
binding.createAccount.isGone = api.createAccountUrl.isNullOrBlank()
binding.createAccount.setOnClickListener {
openBrowser(
api.createAccountUrl ?: return@setOnClickListener,
activity
@ -159,43 +168,43 @@ class SettingsAccount : PreferenceFragmentCompat() {
}
val displayedItems = listOf(
dialog.login_username_input,
dialog.login_email_input,
dialog.login_server_input,
dialog.login_password_input
binding.loginUsernameInput,
binding.loginEmailInput,
binding.loginServerInput,
binding.loginPasswordInput
).filter { it.isVisible }
displayedItems.foldRight(displayedItems.firstOrNull()) { item, previous ->
item?.id?.let { previous?.nextFocusDownId = it }
previous?.id?.let { item?.nextFocusUpId = it }
item.id.let { previous?.nextFocusDownId = it }
previous?.id?.let { item.nextFocusUpId = it }
item
}
displayedItems.firstOrNull()?.let {
dialog.create_account?.nextFocusDownId = it.id
it.nextFocusUpId = dialog.create_account.id
binding.createAccount.nextFocusDownId = it.id
it.nextFocusUpId = binding.createAccount.id
}
dialog.apply_btt?.id?.let {
binding.applyBtt.id.let {
displayedItems.lastOrNull()?.nextFocusDownId = it
}
dialog.text1?.text = api.name
binding.text1.text = api.name
if (api.storesPasswordInPlainText) {
api.getLatestLoginData()?.let { data ->
dialog.login_email_input?.setText(data.email ?: "")
dialog.login_server_input?.setText(data.server ?: "")
dialog.login_username_input?.setText(data.username ?: "")
dialog.login_password_input?.setText(data.password ?: "")
binding.loginEmailInput.setText(data.email ?: "")
binding.loginServerInput.setText(data.server ?: "")
binding.loginUsernameInput.setText(data.username ?: "")
binding.loginPasswordInput.setText(data.password ?: "")
}
}
dialog.apply_btt?.setOnClickListener {
binding.applyBtt.setOnClickListener {
val loginData = InAppAuthAPI.LoginData(
username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null,
password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null,
email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null,
server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null,
username = if (api.requiresUsername) binding.loginUsernameInput.text?.toString() else null,
password = if (api.requiresPassword) binding.loginPasswordInput.text?.toString() else null,
email = if (api.requiresEmail) binding.loginEmailInput.text?.toString() else null,
server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null,
)
ioSafe {
val isSuccessful = try {
@ -207,7 +216,6 @@ class SettingsAccount : PreferenceFragmentCompat() {
activity.runOnUiThread {
try {
showToast(
activity,
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
.format(
api.name
@ -220,10 +228,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
}
dialog.dismissSafe(activity)
}
dialog.cancel_btt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
}
else -> {
throw NotImplementedError("You are trying to add an account that has an unknown login method")
}

View file

@ -8,13 +8,17 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.StringRes
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.appbar.MaterialToolbar
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment
@ -22,16 +26,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.main_settings.*
import kotlinx.android.synthetic.main.standard_toolbar.*
import java.io.File
class SettingsFragment : Fragment() {
companion object {
var beneneCount = 0
private var isTv : Boolean = false
private var isTrueTv : Boolean = false
private var isTv: Boolean = false
private var isTrueTv: Boolean = false
fun PreferenceFragmentCompat?.getPref(id: Int): Preference? {
if (this == null) return null
@ -55,26 +57,31 @@ class SettingsFragment : Fragment() {
fun Fragment?.setUpToolbar(title: String) {
if (this == null) return
settings_toolbar?.apply {
val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
settingsToolbar.apply {
setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener {
activity?.onBackPressed()
}
}
context.fixPaddingStatusbar(settings_toolbar)
fixPaddingStatusbar(settingsToolbar)
}
fun Fragment?.setUpToolbar(@StringRes title: Int) {
if (this == null) return
settings_toolbar?.apply {
val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
settingsToolbar.apply {
setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
setNavigationOnClickListener {
activity?.onBackPressed()
}
}
context.fixPaddingStatusbar(settings_toolbar)
fixPaddingStatusbar(settingsToolbar)
}
fun getFolderSize(dir: File): Long {
@ -139,12 +146,21 @@ class SettingsFragment : Fragment() {
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
var binding: MainSettingsBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.main_settings, container, false)
): View {
val localBinding = MainSettingsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.main_settings, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -152,41 +168,41 @@ class SettingsFragment : Fragment() {
activity?.navigate(id, Bundle())
}
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
val isTrueTv = isTrueTvSettings()
for (syncApi in accountManagers) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue
if (settings_profile_pic?.setImage(
if (binding?.settingsProfilePic?.setImage(
pic,
errorImageDrawable = HomeFragment.errorProfilePic
) == true
) {
settings_profile_text?.text = login.name
settings_profile?.isVisible = true
binding?.settingsProfileText?.text = login.name
binding?.settingsProfile?.isVisible = true
break
}
}
listOf(
Pair(settings_general, R.id.action_navigation_settings_to_navigation_settings_general),
Pair(settings_player, R.id.action_navigation_settings_to_navigation_settings_player),
Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account),
Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
Pair(settings_providers, R.id.action_navigation_settings_to_navigation_settings_providers),
Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates),
Pair(
settings_extensions,
R.id.action_navigation_settings_to_navigation_settings_extensions
),
).forEach { (view, navigationId) ->
view?.apply {
setOnClickListener {
navigate(navigationId)
}
if (isTrueTv) {
isFocusable = true
isFocusableInTouchMode = true
binding?.apply {
listOf(
settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general,
settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player,
settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account,
settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui,
settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers,
settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates,
settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions,
).forEach { (view, navigationId) ->
view.apply {
setOnClickListener {
navigate(navigationId)
}
if (isTrueTv) {
isFocusable = true
isFocusableInTouchMode = true
}
}
}
}

View file

@ -20,8 +20,11 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding
import com.lagradost.cloudstream3.databinding.AddSiteInputBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
@ -38,8 +41,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
import kotlinx.android.synthetic.main.add_remove_sites.*
import kotlinx.android.synthetic.main.add_site_input.*
import java.io.File
fun getCurrentLocale(context: Context): String {
@ -69,6 +70,7 @@ val appLanguages = arrayListOf(
Triple("", "español", "es"),
Triple("", "فارسی", "fa"),
Triple("", "français", "fr"),
Triple("", "galego", "gl"),
Triple("", "हिन्दी", "hi"),
Triple("", "hrvatski", "hr"),
Triple("", "magyar", "hu"),
@ -188,7 +190,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
fun showAdd() {
val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name }
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" },
-1,
@ -197,21 +199,23 @@ class SettingsGeneral : PreferenceFragmentCompat() {
{}) { selection ->
val provider = providers.getOrNull(selection) ?: return@showDialog
val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(layoutInflater,null,false)
val builder =
AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom)
.setView(R.layout.add_site_input)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
dialog.text2?.text = provider.name
dialog.apply_btt?.setOnClickListener {
val name = dialog.site_name_input?.text?.toString()
val url = dialog.site_url_input?.text?.toString()
val lang = dialog.site_lang_input?.text?.toString()
binding.text2.text = provider.name
binding.applyBtt.setOnClickListener {
val name = binding.siteNameInput.text?.toString()
val url = binding.siteUrlInput.text?.toString()
val lang = binding.siteLangInput.text?.toString()
val realLang = if (lang.isNullOrBlank()) provider.lang else lang
if (url.isNullOrBlank() || name.isNullOrBlank() || realLang.length != 2) {
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT)
return@setOnClickListener
}
@ -219,10 +223,12 @@ class SettingsGeneral : PreferenceFragmentCompat() {
val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang)
current.add(newSite)
setKey(USER_PROVIDER_API, current.toTypedArray())
// reload apis
MainActivity.afterPluginsLoadedEvent.invoke(false)
dialog.dismissSafe(activity)
}
dialog.cancel_btt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
}
@ -242,18 +248,19 @@ class SettingsGeneral : PreferenceFragmentCompat() {
}
fun showAddOrDelete() {
val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate(layoutInflater,null,false)
val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.add_remove_sites)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
dialog.add_site?.setOnClickListener {
binding.addSite.setOnClickListener {
showAdd()
dialog.dismissSafe(activity)
}
dialog.remove_site?.setOnClickListener {
binding.removeSite.setOnClickListener {
showDelete()
dialog.dismissSafe(activity)
}

View file

@ -105,8 +105,10 @@ class SettingsProviders : PreferenceFragmentCompat() {
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
activity?.getApiProviderLangSettings()?.let { current ->
val languages = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
val languages = synchronized(APIHolder.apis) {
APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
}
val currentList = current.map {
languages.indexOf(it)

View file

@ -13,6 +13,7 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.LogcatBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
@ -25,7 +26,6 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.android.synthetic.main.logcat.*
import okhttp3.internal.closeQuietly
import java.io.BufferedReader
import java.io.InputStreamReader
@ -60,7 +60,9 @@ class SettingsUpdates : PreferenceFragmentCompat() {
getPref(R.string.show_logcat_key)?.setOnPreferenceClickListener { pref ->
val builder =
AlertDialog.Builder(pref.context, R.style.AlertDialogCustom)
.setView(R.layout.logcat)
val binding = LogcatBinding.inflate(layoutInflater,null,false )
builder.setView(binding.root)
val dialog = builder.create()
dialog.show()
@ -81,9 +83,9 @@ class SettingsUpdates : PreferenceFragmentCompat() {
}
val text = log.toString()
dialog.text1?.text = text
binding.text1.text = text
dialog.copy_btt?.setOnClickListener {
binding.copyBtt.setOnClickListener {
// Can crash on too much text
try {
val serviceClipboard =
@ -93,14 +95,14 @@ class SettingsUpdates : PreferenceFragmentCompat() {
serviceClipboard.setPrimaryClip(clip)
dialog.dismissSafe(activity)
} catch (e: TransactionTooLargeException) {
showToast(activity, R.string.clipboard_too_large)
showToast(R.string.clipboard_too_large)
}
}
dialog.clear_btt?.setOnClickListener {
binding.clearBtt.setOnClickListener {
Runtime.getRuntime().exec("logcat -c")
dialog.dismissSafe(activity)
}
dialog.save_btt?.setOnClickListener {
binding.saveBtt.setOnClickListener {
var fileStream: OutputStream? = null
try {
fileStream =
@ -119,7 +121,7 @@ class SettingsUpdates : PreferenceFragmentCompat() {
dialog.dismissSafe(activity)
}
}
dialog.close_btt?.setOnClickListener {
binding.closeBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
return@setOnPreferenceClickListener true
@ -156,7 +158,6 @@ class SettingsUpdates : PreferenceFragmentCompat() {
if (activity?.runAutoUpdate(false) == false) {
activity?.runOnUiThread {
showToast(
activity,
R.string.no_update_found,
Toast.LENGTH_SHORT
)

View file

@ -18,8 +18,10 @@ import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.databinding.AddRepoInputBinding
import com.lagradost.cloudstream3.databinding.FragmentExtensionsBinding
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@ -30,16 +32,22 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager
import kotlinx.android.synthetic.main.add_repo_input.*
import kotlinx.android.synthetic.main.fragment_extensions.*
class ExtensionsFragment : Fragment() {
var binding: FragmentExtensionsBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.fragment_extensions, container, false)
): View {
val localBinding = FragmentExtensionsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root//inflater.inflate(R.layout.fragment_extensions, container, false)
}
private fun View.setLayoutWidth(weight: Int) {
@ -74,7 +82,7 @@ class ExtensionsFragment : Fragment() {
setUpToolbar(R.string.extensions)
repo_recycler_view?.adapter = RepoAdapter(false, {
binding?.repoRecyclerView?.adapter = RepoAdapter(false, {
findNavController().navigate(
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
PluginsFragment.newInstance(
@ -97,6 +105,7 @@ class ExtensionsFragment : Fragment() {
extensionViewModel.loadRepositories()
}
}
DialogInterface.BUTTON_NEGATIVE -> {}
}
}
@ -112,12 +121,12 @@ class ExtensionsFragment : Fragment() {
})
observe(extensionViewModel.repositories) {
repo_recycler_view?.isVisible = it.isNotEmpty()
blank_repo_screen?.isVisible = it.isEmpty()
(repo_recycler_view?.adapter as? RepoAdapter)?.updateList(it)
binding?.repoRecyclerView?.isVisible = it.isNotEmpty()
binding?.blankRepoScreen?.isVisible = it.isEmpty()
(binding?.repoRecyclerView?.adapter as? RepoAdapter)?.updateList(it)
}
repo_recycler_view?.apply {
binding?.repoRecyclerView?.apply {
context?.let { ctx ->
layoutManager = LinearRecycleViewLayoutManager(ctx, nextFocusUpId, nextFocusDownId)
}
@ -138,32 +147,31 @@ class ExtensionsFragment : Fragment() {
// }
// }
observe(extensionViewModel.pluginStats) {
when (it) {
is Some.Success -> {
val value = it.value
observeNullable(extensionViewModel.pluginStats) { value ->
binding?.apply {
if (value == null) {
pluginStorageAppbar.isVisible = false
plugin_storage_appbar?.isVisible = true
if (value.total == 0) {
plugin_download?.setLayoutWidth(1)
plugin_disabled?.setLayoutWidth(0)
plugin_not_downloaded?.setLayoutWidth(0)
} else {
plugin_download?.setLayoutWidth(value.downloaded)
plugin_disabled?.setLayoutWidth(value.disabled)
plugin_not_downloaded?.setLayoutWidth(value.notDownloaded)
}
plugin_not_downloaded_txt.setText(value.notDownloadedText)
plugin_disabled_txt.setText(value.disabledText)
plugin_download_txt.setText(value.downloadedText)
return@observeNullable
}
is Some.None -> {
plugin_storage_appbar?.isVisible = false
pluginStorageAppbar.isVisible = true
if (value.total == 0) {
pluginDownload.setLayoutWidth(1)
pluginDisabled.setLayoutWidth(0)
pluginNotDownloaded.setLayoutWidth(0)
} else {
pluginDownload.setLayoutWidth(value.downloaded)
pluginDisabled.setLayoutWidth(value.disabled)
pluginNotDownloaded.setLayoutWidth(value.notDownloaded)
}
pluginNotDownloadedTxt.setText(value.notDownloadedText)
pluginDisabledTxt.setText(value.disabledText)
pluginDownloadTxt.setText(value.downloadedText)
}
}
plugin_storage_appbar?.setOnClickListener {
binding?.pluginStorageAppbar?.setOnClickListener {
findNavController().navigate(
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
PluginsFragment.newInstance(
@ -175,16 +183,18 @@ class ExtensionsFragment : Fragment() {
}
val addRepositoryClick = View.OnClickListener {
val ctx = context ?: return@OnClickListener
val binding = AddRepoInputBinding.inflate(LayoutInflater.from(ctx), null, false)
val builder =
AlertDialog.Builder(context ?: return@OnClickListener, R.style.AlertDialogCustom)
.setView(R.layout.add_repo_input)
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
0
)?.text?.toString()?.let { copy ->
dialog.repo_url_input?.setText(copy)
binding.repoUrlInput.setText(copy)
}
// dialog.list_repositories?.setOnClickListener {
@ -194,14 +204,14 @@ class ExtensionsFragment : Fragment() {
// }
// dialog.text2?.text = provider.name
dialog.apply_btt?.setOnClickListener secondListener@{
val name = dialog.repo_name_input?.text?.toString()
binding.applyBtt.setOnClickListener secondListener@{
val name = binding.repoNameInput.text?.toString()
ioSafe {
val url = dialog.repo_url_input?.text?.toString()
val url = binding.repoUrlInput.text?.toString()
?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
if (url.isNullOrBlank()) {
main {
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT)
}
} else {
val fixedName = if (!name.isNullOrBlank()) name
@ -216,22 +226,23 @@ class ExtensionsFragment : Fragment() {
}
dialog.dismissSafe(activity)
}
dialog.cancel_btt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
}
val isTv = isTrueTvSettings()
add_repo_button?.isGone = isTv
add_repo_button_imageview_holder?.isVisible = isTv
binding?.apply {
addRepoButton.isGone = isTv
addRepoButtonImageviewHolder.isVisible = isTv
// Band-aid for Fire TV
plugin_storage_appbar?.isFocusableInTouchMode = isTv
add_repo_button_imageview?.isFocusableInTouchMode = isTv
add_repo_button?.setOnClickListener(addRepositoryClick)
add_repo_button_imageview?.setOnClickListener(addRepositoryClick)
// Band-aid for Fire TV
pluginStorageAppbar.isFocusableInTouchMode = isTv
addRepoButtonImageview.isFocusableInTouchMode = isTv
addRepoButton.setOnClickListener(addRepositoryClick)
addRepoButtonImageview.setOnClickListener(addRepositoryClick)
}
reloadRepositories()
}
}

View file

@ -7,7 +7,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline
@ -40,8 +39,8 @@ class ExtensionsViewModel : ViewModel() {
private val _repositories = MutableLiveData<Array<RepositoryData>>()
val repositories: LiveData<Array<RepositoryData>> = _repositories
private val _pluginStats: MutableLiveData<Some<PluginStats>> = MutableLiveData(Some.None)
val pluginStats: LiveData<Some<PluginStats>> = _pluginStats
private val _pluginStats: MutableLiveData<PluginStats?> = MutableLiveData(null)
val pluginStats: LiveData<PluginStats?> = _pluginStats
//TODO CACHE GET REQUESTS
// DO not use viewModelScope.launchSafe, it will ANR on slow internet
@ -78,7 +77,7 @@ class ExtensionsViewModel : ViewModel() {
debugAssert({ stats.downloaded + stats.notDownloaded + stats.disabled != stats.total }) {
"downloaded(${stats.downloaded}) + notDownloaded(${stats.notDownloaded}) + disabled(${stats.disabled}) != total(${stats.total})"
}
_pluginStats.postValue(Some.Success(stats))
_pluginStats.postValue(stats)
}
private fun repos() = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)

View file

@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.settings.extensions
import android.text.format.Formatter.formatShortFileSize
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
@ -13,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.ui.result.setText
@ -26,10 +26,11 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.repository_item.view.*
import org.junit.Assert
import org.junit.Test
import java.text.DecimalFormat
import kotlin.math.floor
import kotlin.math.log10
data class PluginViewData(
@ -45,8 +46,10 @@ class PluginAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item
val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return PluginViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false)
RepositoryItemBinding.bind(inflated) // may crash
)
}
@ -82,8 +85,10 @@ class PluginAdapter(
// Clear glide image because setImageResource doesn't override
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
holder.itemView.entry_icon?.let { pluginIcon ->
GlideApp.with(pluginIcon).clear(pluginIcon)
if (holder is PluginViewHolder) {
holder.binding.entryIcon.let { pluginIcon ->
GlideApp.with(pluginIcon).clear(pluginIcon)
}
}
super.onViewRecycled(holder)
}
@ -112,7 +117,7 @@ class PluginAdapter(
fun prettyCount(number: Number): String? {
val suffix = charArrayOf(' ', 'k', 'M', 'B', 'T', 'P', 'E')
val numValue = number.toLong()
val value = Math.floor(Math.log10(numValue.toDouble())).toInt()
val value = floor(log10(numValue.toDouble())).toInt()
val base = value / 3
return if (value >= 3 && base < suffix.size) {
DecimalFormat("#0.00").format(
@ -127,8 +132,8 @@ class PluginAdapter(
}
}
inner class PluginViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
inner class PluginViewHolder(val binding: RepositoryItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
data: PluginViewData,
@ -138,17 +143,17 @@ class PluginAdapter(
val name = metadata.name.removeSuffix("Provider")
val alpha = if (disabled) 0.6f else 1f
val isLocal = !data.plugin.second.url.startsWith("http")
itemView.main_text?.alpha = alpha
itemView.sub_text?.alpha = alpha
binding.mainText.alpha = alpha
binding.subText.alpha = alpha
val drawableInt = if (data.isDownloaded)
R.drawable.ic_baseline_delete_outline_24
else R.drawable.netflix_download
itemView.nsfw_marker?.isVisible = metadata.tvTypes?.contains("NSFW") ?: false
itemView.action_button?.setImageResource(drawableInt)
binding.nsfwMarker.isVisible = metadata.tvTypes?.contains("NSFW") ?: false
binding.actionButton.setImageResource(drawableInt)
itemView.action_button?.setOnClickListener {
binding.actionButton.setOnClickListener {
iconClickCallback.invoke(data.plugin)
}
itemView.setOnClickListener {
@ -169,10 +174,11 @@ class PluginAdapter(
if (data.isDownloaded) {
// On local plugins page the filepath is provided instead of url.
val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
val plugin =
PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
if (plugin?.openSettings != null) {
itemView.action_settings?.isVisible = true
itemView.action_settings.setOnClickListener {
binding.actionSettings.isVisible = true
binding.actionSettings.setOnClickListener {
try {
plugin.openSettings!!.invoke(itemView.context)
} catch (e: Throwable) {
@ -185,13 +191,13 @@ class PluginAdapter(
}
}
} else {
itemView.action_settings?.isVisible = false
binding.actionSettings.isVisible = false
}
} else {
itemView.action_settings?.isVisible = false
binding.actionSettings.isVisible = false
}
if (itemView.entry_icon?.setImage(//itemView.entry_icon?.height ?:
if (!binding.entryIcon.setImage(//itemView.entry_icon?.height ?:
metadata.iconUrl?.replace(
"%size%",
"$iconSize"
@ -201,41 +207,47 @@ class PluginAdapter(
),
null,
errorImageDrawable = R.drawable.ic_baseline_extension_24
) != true
)
) {
itemView.entry_icon?.setImageResource(R.drawable.ic_baseline_extension_24)
binding.entryIcon.setImageResource(R.drawable.ic_baseline_extension_24)
}
itemView.ext_version?.isVisible = true
itemView.ext_version?.text = "v${metadata.version}"
binding.extVersion.isVisible = true
binding.extVersion.text = "v${metadata.version}"
if (metadata.language.isNullOrBlank()) {
itemView.lang_icon?.isVisible = false
binding.langIcon.isVisible = false
} else {
itemView.lang_icon?.isVisible = true
itemView.lang_icon.text = "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
binding.langIcon.isVisible = true
binding.langIcon.text =
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
}
itemView.ext_votes?.isVisible = false
binding.extVotes.isVisible = false
if (!isLocal) {
ioSafe {
metadata.getVotes().main {
itemView.ext_votes?.setText(txt(R.string.extension_rating, prettyCount(it)))
itemView.ext_votes?.isVisible = true
binding.extVotes.setText(txt(R.string.extension_rating, prettyCount(it)))
binding.extVotes.isVisible = true
}
}
}
if (metadata.fileSize != null) {
itemView.ext_filesize?.isVisible = true
itemView.ext_filesize?.text = formatShortFileSize(itemView.context, metadata.fileSize)
binding.extFilesize.isVisible = true
binding.extFilesize.text = formatShortFileSize(itemView.context, metadata.fileSize)
} else {
itemView.ext_filesize?.isVisible = false
binding.extFilesize.isVisible = false
}
itemView.main_text.setText(if(disabled) txt(R.string.single_plugin_disabled, name) else txt(name))
itemView.sub_text?.isGone = metadata.description.isNullOrBlank()
itemView.sub_text?.text = metadata.description.html()
binding.mainText.setText(
if (disabled) txt(
R.string.single_plugin_disabled,
name
) else txt(name)
)
binding.subText.isGone = metadata.description.isNullOrBlank()
binding.subText.text = metadata.description.html()
}
}
}

View file

@ -2,30 +2,29 @@ package com.lagradost.cloudstream3.ui.settings.extensions
import android.content.res.ColorStateList
import android.os.Bundle
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import android.text.format.Formatter.formatFileSize
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.fragment_plugin_details.*
import android.text.format.Formatter.formatFileSize
import android.util.Log
import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentPluginDetailsBinding
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi
import com.lagradost.cloudstream3.plugins.VotingApi.canVote
import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.plugins.VotingApi.vote
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi.canVote
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import kotlinx.android.synthetic.main.repository_item.view.*
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragment() {
@ -43,18 +42,27 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
var binding: FragmentPluginDetailsBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_plugin_details, container, false)
): View {
val localBinding = FragmentPluginDetailsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.fragment_plugin_details, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val metadata = data.plugin.second
if (plugin_icon?.setImage(//plugin_icon?.height ?:
binding?.apply {
if (!pluginIcon.setImage(//plugin_icon?.height ?:
metadata.iconUrl?.replace(
"%size%",
"$iconSize"
@ -64,23 +72,33 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
),
null,
errorImageDrawable = R.drawable.ic_baseline_extension_24
) != true
)
) {
plugin_icon?.setImageResource(R.drawable.ic_baseline_extension_24)
pluginIcon.setImageResource(R.drawable.ic_baseline_extension_24)
}
plugin_name?.text = metadata.name.removeSuffix("Provider")
plugin_version?.text = metadata.version.toString()
plugin_description?.text = metadata.description ?: getString(R.string.no_data)
plugin_size?.text = if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(context, metadata.fileSize)
plugin_author?.text = if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(", ")
plugin_status?.text = resources.getStringArray(R.array.extension_statuses)[metadata.status]
plugin_types?.text = if ((metadata.tvTypes == null) || metadata.tvTypes.isEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(", ")
plugin_lang?.text = if (metadata.language == null)
getString(R.string.no_data)
pluginName.text = metadata.name.removeSuffix("Provider")
pluginVersion.text = metadata.version.toString()
pluginDescription.text = metadata.description ?: getString(R.string.no_data)
pluginSize.text =
if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(
context,
metadata.fileSize
)
pluginAuthor.text =
if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(
", "
)
pluginStatus.text = resources.getStringArray(R.array.extension_statuses)[metadata.status]
pluginTypes.text =
if (metadata.tvTypes.isNullOrEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(
", "
)
pluginLang.text = if (metadata.language == null)
getString(R.string.no_data)
else
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
github_btn.setOnClickListener {
githubBtn.setOnClickListener {
if (metadata.repositoryUrl != null) {
openBrowser(metadata.repositoryUrl)
}
@ -93,10 +111,11 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
if (data.isDownloaded) {
// On local plugins page the filepath is provided instead of url.
val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
val plugin =
PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
if (plugin?.openSettings != null && context != null) {
action_settings?.isVisible = true
action_settings.setOnClickListener {
actionSettings.isVisible = true
actionSettings.setOnClickListener {
try {
plugin.openSettings!!.invoke(requireContext())
} catch (e: Throwable) {
@ -109,10 +128,10 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
}
}
} else {
action_settings?.isVisible = false
actionSettings.isVisible = false
}
} else {
action_settings?.isVisible = false
actionSettings.isVisible = false
}
upvote.setOnClickListener {
@ -136,23 +155,40 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
updateVoting(it)
}
}
}
}
private fun updateVoting(value: Int) {
val metadata = data.plugin.second
plugin_votes.text = value.toString()
when (metadata.getVoteType()) {
VotingApi.VoteType.UPVOTE -> {
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary)
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
}
VotingApi.VoteType.DOWNVOTE -> {
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary)
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
}
VotingApi.VoteType.NONE -> {
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
binding?.apply {
pluginVotes.text = value.toString()
when (metadata.getVoteType()) {
VotingApi.VoteType.UPVOTE -> {
upvote.imageTintList = ColorStateList.valueOf(
context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary
)
downvote.imageTintList = ColorStateList.valueOf(
context?.colorFromAttribute(R.attr.white) ?: R.color.white
)
}
VotingApi.VoteType.DOWNVOTE -> {
downvote.imageTintList = ColorStateList.valueOf(
context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary
)
upvote.imageTintList = ColorStateList.valueOf(
context?.colorFromAttribute(R.attr.white) ?: R.color.white
)
}
VotingApi.VoteType.NONE -> {
upvote.imageTintList = ColorStateList.valueOf(
context?.colorFromAttribute(R.attr.white) ?: R.color.white
)
downvote.imageTintList = ColorStateList.valueOf(
context?.colorFromAttribute(R.attr.white) ?: R.color.white
)
}
}
}
}

View file

@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
@ -20,9 +21,6 @@ import com.lagradost.cloudstream3.ui.settings.appLanguages
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.fragment_plugins.*
import kotlinx.android.synthetic.main.tvtypes_chips.*
import kotlinx.android.synthetic.main.tvtypes_chips_scroll.*
const val PLUGINS_BUNDLE_NAME = "name"
const val PLUGINS_BUNDLE_URL = "url"
@ -33,11 +31,19 @@ class PluginsFragment : Fragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.fragment_plugins, container, false)
): View {
val localBinding = FragmentPluginsBinding.inflate(inflater,container,false)
binding = localBinding
return localBinding.root//inflater.inflate(R.layout.fragment_plugins, container, false)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
private val pluginViewModel: PluginsViewModel by activityViewModels()
var binding: FragmentPluginsBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -66,8 +72,8 @@ class PluginsFragment : Fragment() {
}
setUpToolbar(name)
settings_toolbar?.setOnMenuItemClickListener { menuItem ->
binding?.settingsToolbar?.apply {
setOnMenuItemClickListener { menuItem ->
when (menuItem?.itemId) {
R.id.download_all -> {
PluginsViewModel.downloadAll(activity, url, pluginViewModel)
@ -99,67 +105,69 @@ class PluginsFragment : Fragment() {
}
val searchView =
settings_toolbar?.menu?.findItem(R.id.search_button)?.actionView as? SearchView
menu?.findItem(R.id.search_button)?.actionView as? SearchView
// Don't go back if active query
settings_toolbar?.setNavigationOnClickListener {
setNavigationOnClickListener {
if (searchView?.isIconified == false) {
searchView.isIconified = true
} else {
activity?.onBackPressed()
}
}
searchView?.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (!hasFocus) pluginViewModel.search(null)
}
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
pluginViewModel.search(query)
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
pluginViewModel.search(newText)
return true
}
})
}
// searchView?.onActionViewCollapsed = {
// pluginViewModel.search(null)
// }
// Because onActionViewCollapsed doesn't wanna work we need this workaround :(
searchView?.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (!hasFocus) pluginViewModel.search(null)
}
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
pluginViewModel.search(query)
return true
}
override fun onQueryTextChange(newText: String?): Boolean {
pluginViewModel.search(newText)
return true
}
})
plugin_recycler_view?.adapter =
binding?.pluginRecyclerView?.adapter =
PluginAdapter {
pluginViewModel.handlePluginAction(activity, url, it, isLocal)
}
if (isTvSettings()) {
// Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that.
plugin_recycler_view?.setPadding(0, 0, 0, 200.toPx)
binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx)
}
observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
(plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list)
(binding?.pluginRecyclerView?.adapter as? PluginAdapter)?.updateList(list)
if (scrollToTop)
plugin_recycler_view?.scrollToPosition(0)
binding?.pluginRecyclerView?.scrollToPosition(0)
}
if (isLocal) {
// No download button and no categories on local
settings_toolbar?.menu?.findItem(R.id.download_all)?.isVisible = false
settings_toolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false
binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false
binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false
pluginViewModel.updatePluginListLocal()
tv_types_scroll_view?.isVisible = false
binding?.tvtypesChipsScroll?.root?.isVisible = false
} else {
pluginViewModel.updatePluginList(context, url)
tv_types_scroll_view?.isVisible = true
binding?.tvtypesChipsScroll?.root?.isVisible = true
bindChips(home_select_group, emptyList(), TvType.values().toList()) { list ->
bindChips(binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), TvType.values().toList()) { list ->
pluginViewModel.tvTypes.clear()
pluginViewModel.tvTypes.addAll(list.map { it.name })
pluginViewModel.updateFilteredPlugins()

View file

@ -86,7 +86,6 @@ class PluginsViewModel : ViewModel() {
}.also { list ->
main {
showToast(
activity,
if (list.isEmpty()) {
txt(
R.string.batch_download_nothing_to_download_format,
@ -113,7 +112,6 @@ class PluginsViewModel : ViewModel() {
}.main { list ->
if (list.any { it }) {
showToast(
activity,
txt(
R.string.batch_download_finish_format,
list.count { it },
@ -123,7 +121,7 @@ class PluginsViewModel : ViewModel() {
)
viewModel?.updatePluginListPrivate(activity, repositoryUrl)
} else if (list.isNotEmpty()) {
showToast(activity, R.string.download_failed, Toast.LENGTH_SHORT)
showToast(R.string.download_failed, Toast.LENGTH_SHORT)
}
}
}
@ -166,9 +164,9 @@ class PluginsViewModel : ViewModel() {
runOnMainThread {
if (success)
showToast(activity, message, Toast.LENGTH_SHORT)
showToast(message, Toast.LENGTH_SHORT)
else
showToast(activity, R.string.error, Toast.LENGTH_SHORT)
showToast(R.string.error, Toast.LENGTH_SHORT)
}
if (success)

View file

@ -1,14 +1,15 @@
package com.lagradost.cloudstream3.ui.settings.extensions
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import kotlinx.android.synthetic.main.repository_item.view.*
class RepoAdapter(
val isSetup: Boolean,
@ -20,9 +21,17 @@ class RepoAdapter(
private val repositories: MutableList<RepositoryData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item
val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
) else RepositoryItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
) //R.layout.repository_item_tv else R.layout.repository_item
return RepoViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false)
layout
)
}
@ -57,30 +66,57 @@ class RepoAdapter(
diffResult.dispatchUpdatesTo(this)
}
inner class RepoViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
inner class RepoViewHolder(
val binding: ViewBinding
) :
RecyclerView.ViewHolder(binding.root) {
fun bind(
repositoryData: RepositoryData
) {
val isPrebuilt = PREBUILT_REPOSITORIES.contains(repositoryData)
val drawable =
if (isSetup) R.drawable.netflix_download else R.drawable.ic_baseline_delete_outline_24
when (binding) {
is RepositoryItemTvBinding -> {
binding.apply {
// Only shows icon if on setup or if it isn't a prebuilt repo.
// No delete buttons on prebuilt repos.
if (!isPrebuilt || isSetup) {
actionButton.setImageResource(drawable)
}
// Only shows icon if on setup or if it isn't a prebuilt repo.
// No delete buttons on prebuilt repos.
if (!isPrebuilt || isSetup) {
itemView.action_button?.setImageResource(drawable)
}
actionButton.setOnClickListener {
imageClickCallback(repositoryData)
}
itemView.action_button?.setOnClickListener {
imageClickCallback(repositoryData)
}
repositoryItemRoot.setOnClickListener {
clickCallback(repositoryData)
}
mainText.text = repositoryData.name
subText.text = repositoryData.url
}
}
itemView.repository_item_root?.setOnClickListener {
clickCallback(repositoryData)
is RepositoryItemBinding -> {
binding.apply {
// Only shows icon if on setup or if it isn't a prebuilt repo.
// No delete buttons on prebuilt repos.
if (!isPrebuilt || isSetup) {
actionButton.setImageResource(drawable)
}
actionButton.setOnClickListener {
imageClickCallback(repositoryData)
}
repositoryItemRoot.setOnClickListener {
clickCallback(repositoryData)
}
mainText.text = repositoryData.name
subText.text = repositoryData.url
}
}
}
itemView.main_text?.text = repositoryData.name
itemView.sub_text?.text = repositoryData.url
}
}
}

View file

@ -1,97 +1,105 @@
package com.lagradost.cloudstream3.ui.settings.testing
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentTestingBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import kotlinx.android.synthetic.main.fragment_testing.*
import kotlinx.android.synthetic.main.view_test.*
class TestFragment : Fragment() {
private val testViewModel: TestViewModel by activityViewModels()
var binding: FragmentTestingBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setUpToolbar(R.string.category_provider_test)
super.onViewCreated(view, savedInstanceState)
provider_test_recycler_view?.adapter = TestResultAdapter(
mutableListOf()
)
binding?.apply {
providerTestRecyclerView.adapter = TestResultAdapter(
mutableListOf()
)
testViewModel.init()
if (testViewModel.isRunningTest) {
provider_test?.setState(TestView.TestState.Running)
}
observe(testViewModel.providerProgress) { (passed, failed, total) ->
provider_test?.setProgress(passed, failed, total)
}
observeNullable(testViewModel.providerResults) {
normalSafeApiCall {
val newItems = it.sortedBy { api -> api.first.name }
(provider_test_recycler_view?.adapter as? TestResultAdapter)?.updateList(
newItems
)
testViewModel.init()
if (testViewModel.isRunningTest) {
providerTest.setState(TestView.TestState.Running)
}
}
provider_test?.setOnPlayButtonListener { state ->
when (state) {
TestView.TestState.Stopped -> testViewModel.stopTest()
TestView.TestState.Running -> testViewModel.startTest()
TestView.TestState.None -> testViewModel.startTest()
observe(testViewModel.providerProgress) { (passed, failed, total) ->
providerTest.setProgress(passed, failed, total)
}
}
if (isTrueTvSettings()) {
tests_play_pause?.isFocusableInTouchMode = true
tests_play_pause?.requestFocus()
}
provider_test?.playPauseButton?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
provider_test_appbar?.setExpanded(true, true)
observeNullable(testViewModel.providerResults) {
normalSafeApiCall {
val newItems = it.sortedBy { api -> api.first.name }
(providerTestRecyclerView.adapter as? TestResultAdapter)?.updateList(
newItems
)
}
}
providerTest.setOnPlayButtonListener { state ->
when (state) {
TestView.TestState.Stopped -> testViewModel.stopTest()
TestView.TestState.Running -> testViewModel.startTest()
TestView.TestState.None -> testViewModel.startTest()
}
}
}
fun focusRecyclerView() {
// Hack to make it possible to focus the recyclerview.
if (isTrueTvSettings()) {
provider_test_recycler_view?.requestFocus()
provider_test_appbar?.setExpanded(false, true)
providerTest.playPauseButton?.isFocusableInTouchMode = true
providerTest.playPauseButton?.requestFocus()
}
}
provider_test?.setOnMainClick {
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All)
focusRecyclerView()
}
provider_test?.setOnFailedClick {
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed)
focusRecyclerView()
}
provider_test?.setOnPassedClick {
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed)
focusRecyclerView()
providerTest.playPauseButton?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
providerTestAppbar.setExpanded(true, true)
}
}
fun focusRecyclerView() {
// Hack to make it possible to focus the recyclerview.
if (isTrueTvSettings()) {
providerTestRecyclerView.requestFocus()
providerTestAppbar.setExpanded(false, true)
}
}
providerTest.setOnMainClick {
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All)
focusRecyclerView()
}
providerTest.setOnFailedClick {
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed)
focusRecyclerView()
}
providerTest.setOnPassedClick {
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed)
focusRecyclerView()
}
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_testing, container, false)
): View {
val localBinding = FragmentTestingBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root//inflater.inflate(R.layout.fragment_testing, container, false)
}
}

View file

@ -10,19 +10,20 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding
import com.lagradost.cloudstream3.mvvm.getAllMessages
import com.lagradost.cloudstream3.mvvm.getStackTracePretty
import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.android.synthetic.main.provider_test_item.view.*
class TestResultAdapter(override val items: MutableList<Pair<MainAPI, TestingUtils.TestResultProvider>>) :
AppUtils.DiffAdapter<Pair<MainAPI, TestingUtils.TestResultProvider>>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ProviderTestViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.provider_test_item, parent, false),
ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false)
//LayoutInflater.from(parent.context)
// .inflate(R.layout.provider_test_item, parent, false),
)
}
@ -35,12 +36,12 @@ class TestResultAdapter(override val items: MutableList<Pair<MainAPI, TestingUti
}
}
inner class ProviderTestViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val languageText: TextView = itemView.lang_icon
private val providerTitle: TextView = itemView.main_text
private val statusText: TextView = itemView.passed_failed_marker
private val failDescription: TextView = itemView.fail_description
private val logButton: ImageView = itemView.action_button
inner class ProviderTestViewHolder(binding: ProviderTestItemBinding) : RecyclerView.ViewHolder(binding.root) {
private val languageText: TextView = binding.langIcon
private val providerTitle: TextView = binding.mainText
private val statusText: TextView = binding.passedFailedMarker
private val failDescription: TextView = binding.failDescription
private val logButton: ImageView = binding.actionButton
private fun String.lastLine(): String? {
return this.lines().lastOrNull { it.isNotBlank() }

View file

@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import okhttp3.internal.toImmutableList
class TestViewModel : ViewModel() {
data class TestProgress(
@ -81,15 +82,14 @@ class TestViewModel : ViewModel() {
}
fun init() {
val apis = APIHolder.allProviders
total = apis.size
total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
updateProgress()
}
fun startTest() {
scope = CoroutineScope(Dispatchers.Default)
val apis = APIHolder.allProviders
val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
total = apis.size
failed = 0
passed = 0

View file

@ -8,21 +8,16 @@ import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentSetupExtensionsBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel
import com.lagradost.cloudstream3.ui.settings.extensions.RepoAdapter
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_extensions.blank_repo_screen
import kotlinx.android.synthetic.main.fragment_extensions.repo_recycler_view
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
import kotlinx.android.synthetic.main.fragment_setup_media.setup_root
class SetupFragmentExtensions : Fragment() {
@ -39,13 +34,24 @@ class SetupFragmentExtensions : Fragment() {
}
}
var binding: FragmentSetupExtensionsBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_setup_extensions, container, false)
): View {
val localBinding = FragmentSetupExtensionsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.fragment_setup_extensions, container, false)
}
override fun onResume() {
super.onResume()
afterRepositoryLoadedEvent += ::setRepositories
@ -60,12 +66,12 @@ class SetupFragmentExtensions : Fragment() {
main {
val repositories = RepositoryManager.getRepositories() + PREBUILT_REPOSITORIES
val hasRepos = repositories.isNotEmpty()
repo_recycler_view?.isVisible = hasRepos
blank_repo_screen?.isVisible = !hasRepos
binding?.repoRecyclerView?.isVisible = hasRepos
binding?.blankRepoScreen?.isVisible = !hasRepos
// view_public_repositories_button?.isVisible = hasRepos
if (hasRepos) {
repo_recycler_view?.adapter = RepoAdapter(true, {}, {
binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, {
PluginsViewModel.downloadAll(activity, it.url, null)
}).apply { updateList(repositories) }
}
@ -80,39 +86,40 @@ class SetupFragmentExtensions : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root)
fixPaddingStatusbar(binding?.setupRoot)
val isSetup = arguments?.getBoolean(SETUP_EXTENSION_BUNDLE_IS_SETUP) ?: false
// view_public_repositories_button?.setOnClickListener {
// openBrowser(PUBLIC_REPOSITORIES_LIST, isTvSettings(), this)
// }
with(context) {
if (this == null) return
normalSafeApiCall {
// val ctx = context ?: return@normalSafeApiCall
setRepositories()
binding?.apply {
if (!isSetup) {
nextBtt.setText(R.string.setup_done)
}
prevBtt.isVisible = isSetup
if (!isSetup) {
next_btt.setText(R.string.setup_done)
}
prev_btt?.isVisible = isSetup
nextBtt.setOnClickListener {
// Continue setup
if (isSetup)
if (
// If any available languages
synchronized(apis) { apis.distinctBy { it.lang }.size > 1 }
) {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
}
else
findNavController().navigate(R.id.navigation_home)
}
next_btt?.setOnClickListener {
// Continue setup
if (isSetup)
if (
// If any available languages
apis.distinctBy { it.lang }.size > 1
) {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
}
else
findNavController().navigate(R.id.navigation_home)
}
prev_btt?.setOnClickListener {
findNavController().navigate(R.id.navigation_setup_language)
prevBtt.setOnClickListener {
findNavController().navigate(R.id.navigation_setup_language)
}
}
}
}

View file

@ -13,40 +13,49 @@ import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentSetupLanguageBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.settings.appLanguages
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_language.*
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
const val HAS_DONE_SETUP_KEY = "HAS_DONE_SETUP"
class SetupFragmentLanguage : Fragment() {
var binding: FragmentSetupLanguageBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_setup_language, container, false)
): View {
val localBinding = FragmentSetupLanguageBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.fragment_setup_language, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root)
// We don't want a crash for all users
normalSafeApiCall {
with(context) {
if (this == null) return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
fixPaddingStatusbar(binding?.setupRoot)
val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
val ctx = context ?: return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val arrayAdapter =
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
binding?.apply {
// Icons may crash on some weird android versions?
normalSafeApiCall {
val drawable = when {
@ -54,10 +63,10 @@ class SetupFragmentLanguage : Fragment() {
BuildConfig.BUILD_TYPE == "prerelease" -> R.drawable.cloud_2_gradient_beta
else -> R.drawable.cloud_2_gradient
}
app_icon_image?.setImageDrawable(ContextCompat.getDrawable(this, drawable))
appIconImage.setImageDrawable(ContextCompat.getDrawable(ctx, drawable))
}
val current = getCurrentLocale(this)
val current = getCurrentLocale(ctx)
val languageCodes = appLanguages.map { it.third }
val languageNames = appLanguages.map { (emoji, name, iso) ->
val flag = emoji.ifBlank { SubtitleHelper.getFlagFromIso(iso) ?: "ERROR" }
@ -66,18 +75,19 @@ class SetupFragmentLanguage : Fragment() {
val index = languageCodes.indexOf(current)
arrayAdapter.addAll(languageNames)
listview1?.adapter = arrayAdapter
listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listview1?.setItemChecked(index, true)
listview1.adapter = arrayAdapter
listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listview1.setItemChecked(index, true)
listview1?.setOnItemClickListener { _, _, position, _ ->
listview1.setOnItemClickListener { _, _, position, _ ->
val code = languageCodes[position]
CommonActivity.setLocale(activity, code)
settingsManager.edit().putString(getString(R.string.locale_key), code).apply()
settingsManager.edit().putString(getString(R.string.locale_key), code)
.apply()
activity?.recreate()
}
next_btt?.setOnClickListener {
nextBtt.setOnClickListener {
// If no plugins go to plugins page
val nextDestination = if (
PluginManager.getPluginsOnline().isEmpty()
@ -92,10 +102,11 @@ class SetupFragmentLanguage : Fragment() {
)
}
skip_btt?.setOnClickListener {
skipBtt.setOnClickListener {
findNavController().navigate(R.id.navigation_home)
}
}
}
}

View file

@ -10,30 +10,39 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_layout.*
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
import kotlinx.android.synthetic.main.fragment_setup_media.setup_root
import org.acra.ACRA
class SetupFragmentLayout : Fragment() {
var binding: FragmentSetupLayoutBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_setup_layout, container, false)
): View {
val localBinding = FragmentSetupLayoutBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.fragment_setup_layout, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root)
fixPaddingStatusbar(binding?.setupRoot)
with(context) {
if (this == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
normalSafeApiCall {
val ctx = context ?: return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = resources.getStringArray(R.array.app_layout)
val prefValues = resources.getIntArray(R.array.app_layout_values)
@ -42,48 +51,48 @@ class SetupFragmentLayout : Fragment() {
settingsManager.getInt(getString(R.string.app_layout_key), -1)
val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
arrayAdapter.addAll(prefNames.toList())
listview1?.adapter = arrayAdapter
listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listview1?.setItemChecked(
prefValues.indexOf(currentLayout), true
)
listview1?.setOnItemClickListener { _, _, position, _ ->
settingsManager.edit()
.putInt(getString(R.string.app_layout_key), prefValues[position])
.apply()
activity?.recreate()
}
acra_switch?.setOnCheckedChangeListener { _, enableCrashReporting ->
// Use same pref as in settings
settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting)
.apply()
val text =
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
crash_reporting_text?.text = getText(text)
}
val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true)
acra_switch.isChecked = enableCrashReporting
crash_reporting_text.text =
getText(
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
binding?.apply {
listview1.adapter = arrayAdapter
listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listview1.setItemChecked(
prefValues.indexOf(currentLayout), true
)
listview1.setOnItemClickListener { _, _, position, _ ->
settingsManager.edit()
.putInt(getString(R.string.app_layout_key), prefValues[position])
.apply()
activity?.recreate()
}
acraSwitch.setOnCheckedChangeListener { _, enableCrashReporting ->
// Use same pref as in settings
settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting)
.apply()
val text =
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
crashReportingText.text = getText(text)
}
next_btt?.setOnClickListener {
findNavController().navigate(R.id.navigation_home)
}
val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true)
prev_btt?.setOnClickListener {
findNavController().popBackStack()
acraSwitch.isChecked = enableCrashReporting
crashReportingText.text =
getText(
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
)
nextBtt.setOnClickListener {
findNavController().navigate(R.id.navigation_home)
}
prevBtt.setOnClickListener {
findNavController().popBackStack()
}
}
}
}
}

View file

@ -10,72 +10,85 @@ import androidx.core.util.forEach
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_media.*
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
class SetupFragmentMedia : Fragment() {
var binding: FragmentSetupMediaBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_setup_media, container, false)
): View {
val localBinding = FragmentSetupMediaBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.fragment_setup_media, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root)
normalSafeApiCall {
fixPaddingStatusbar(binding?.setupRoot)
with(context) {
if (this == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val ctx = context ?: return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
val names = enumValues<TvType>().sorted().map { it.name }
val selected = mutableListOf<Int>()
arrayAdapter.addAll(names)
listview1?.let {
it.adapter = arrayAdapter
it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
binding?.apply {
listview1.let {
it.adapter = arrayAdapter
it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
it.setOnItemClickListener { _, _, _, _ ->
it.checkedItemPositions?.forEach { key, value ->
if (value) {
selected.add(key)
} else {
selected.remove(key)
it.setOnItemClickListener { _, _, _, _ ->
it.checkedItemPositions?.forEach { key, value ->
if (value) {
selected.add(key)
} else {
selected.remove(key)
}
}
val prefValues = selected.mapNotNull { pos ->
val item =
it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null
val itemVal = TvType.valueOf(item)
itemVal.ordinal.toString()
}.toSet()
settingsManager.edit()
.putStringSet(getString(R.string.prefer_media_type_key), prefValues)
.apply()
// Regenerate set homepage
removeKey(USER_SELECTED_HOMEPAGE_API)
}
val prefValues = selected.mapNotNull { pos ->
val item = it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null
val itemVal = TvType.valueOf(item)
itemVal.ordinal.toString()
}.toSet()
settingsManager.edit()
.putStringSet(getString(R.string.prefer_media_type_key), prefValues)
.apply()
// Regenerate set homepage
removeKey(USER_SELECTED_HOMEPAGE_API)
}
}
next_btt?.setOnClickListener {
findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout)
}
nextBtt.setOnClickListener {
findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout)
}
prev_btt?.setOnClickListener {
findNavController().popBackStack()
prevBtt.setOnClickListener {
findNavController().popBackStack()
}
}
}
}
}

View file

@ -14,33 +14,45 @@ import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentSetupProviderLanguagesBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_media.*
class SetupFragmentProviderLanguage : Fragment() {
var binding: FragmentSetupProviderLanguagesBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_setup_provider_languages, container, false)
): View {
val localBinding = FragmentSetupProviderLanguagesBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.fragment_setup_provider_languages, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root)
fixPaddingStatusbar(binding?.setupRoot)
with(context) {
if (this == null) return
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
normalSafeApiCall {
val ctx = context ?: return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
val current = this.getApiProviderLangSettings()
val langs = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
val current = ctx.getApiProviderLangSettings()
val langs = synchronized(APIHolder.apis) { APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName}
val currentList =
current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO
@ -56,31 +68,31 @@ class SetupFragmentProviderLanguage : Fragment() {
}
arrayAdapter.addAll(languageNames)
listview1?.adapter = arrayAdapter
listview1?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
binding?.apply {
listview1.adapter = arrayAdapter
listview1.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
currentList.forEach {
listview1.setItemChecked(it, true)
}
listview1?.setOnItemClickListener { _, _, _, _ ->
listview1.setOnItemClickListener { _, _, _, _ ->
val currentLanguages = mutableListOf<String>()
listview1?.checkedItemPositions?.forEach { key, value ->
listview1.checkedItemPositions?.forEach { key, value ->
if (value) currentLanguages.add(langs[key])
}
settingsManager.edit().putStringSet(
this.getString(R.string.provider_lang_key),
ctx.getString(R.string.provider_lang_key),
currentLanguages.toSet()
).apply()
}
next_btt?.setOnClickListener {
nextBtt.setOnClickListener {
findNavController().navigate(R.id.navigation_setup_provider_languages_to_navigation_setup_media)
}
prev_btt?.setOnClickListener {
prevBtt.setOnClickListener {
findNavController().popBackStack()
}
} }
}
}

View file

@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
@ -31,7 +32,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.subtitle_settings.*
const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings"
@ -137,12 +137,21 @@ class ChromecastSubtitlesFragment : Fragment() {
//subtitle_text?.setStyle(fromSaveToStyle(state))
}
var binding : ChromecastSubtitleSettingsBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.chromecast_subtitle_settings, container, false)
): View {
val localBinding = ChromecastSubtitleSettingsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root//inflater.inflate(R.layout.chromecast_subtitle_settings, container, false)
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
private lateinit var state: SaveChromeCaptionStyle
@ -159,7 +168,7 @@ class ChromecastSubtitlesFragment : Fragment() {
onColorSelectedEvent += ::onColorSelected
onDialogDismissedEvent += ::onDialogDismissed
context?.fixPaddingStatusbar(subs_root)
fixPaddingStatusbar(binding?.subsRoot)
state = getCurrentSavedStyle()
context?.updateState()
@ -185,22 +194,25 @@ class ChromecastSubtitlesFragment : Fragment() {
this.setOnLongClickListener {
it.context.setColor(id, null)
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
}
subs_text_color.setup(0)
subs_outline_color.setup(1)
subs_background_color.setup(2)
binding?.apply {
subsTextColor.setup(0)
subsOutlineColor.setup(1)
subsBackgroundColor.setup(2)
}
val dismissCallback = {
if (hide)
activity?.hideSystemUI()
}
subs_edge_type.setFocusableInTv()
subs_edge_type.setOnClickListener { textView ->
binding?.subsEdgeType?.setFocusableInTv()
binding?.subsEdgeType?.setOnClickListener { textView ->
val edgeTypes = listOf(
Pair(
EDGE_TYPE_NONE,
@ -237,15 +249,15 @@ class ChromecastSubtitlesFragment : Fragment() {
}
}
subs_edge_type.setOnLongClickListener {
binding?.subsEdgeType?.setOnLongClickListener {
state.edgeType = defaultState.edgeType
it.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subs_font_size.setFocusableInTv()
subs_font_size.setOnClickListener { textView ->
binding?.subsFontSize?.setFocusableInTv()
binding?.subsFontSize?.setOnClickListener { textView ->
val fontSizes = listOf(
Pair(0.75f, "75%"),
Pair(0.80f, "80%"),
@ -278,24 +290,26 @@ class ChromecastSubtitlesFragment : Fragment() {
}
}
subs_font_size.setOnLongClickListener { _ ->
binding?.subsFontSize?.setOnLongClickListener { _ ->
state.fontScale = defaultState.fontScale
//textView.context.updateState() // font size not changed
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subs_font.setFocusableInTv()
subs_font.setOnClickListener { textView ->
binding?.subsFont?.setFocusableInTv()
binding?.subsFont?.setOnClickListener { textView ->
val fontTypes = listOf(
Pair(null, textView.context.getString(R.string.normal)),
Pair("Droid Sans", "Droid Sans"),
Pair("Droid Sans Mono", "Droid Sans Mono"),
Pair("Droid Serif Regular", "Droid Serif Regular"),
Pair("Cutive Mono", "Cutive Mono"),
Pair("Short Stack", "Short Stack"),
Pair("Quintessential", "Quintessential"),
Pair("Alegreya Sans SC", "Alegreya Sans SC"),
null to textView.context.getString(R.string.normal),
"Droid Sans" to "Droid Sans",
"Droid Sans Mono" to "Droid Sans Mono",
"Droid Serif Regular" to "Droid Serif Regular",
"Cutive Mono" to "Cutive Mono",
"Short Stack" to "Short Stack",
"Quintessential" to "Quintessential",
"Alegreya Sans SC" to "Alegreya Sans SC",
)
//showBottomDialog
@ -310,35 +324,35 @@ class ChromecastSubtitlesFragment : Fragment() {
textView.context.updateState()
}
}
subs_font.setOnLongClickListener { textView ->
binding?.subsFont?.setOnLongClickListener { textView ->
state.fontFamily = defaultState.fontFamily
textView.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
cancel_btt.setOnClickListener {
binding?.cancelBtt?.setOnClickListener {
activity?.popCurrentPage()
}
apply_btt.setOnClickListener {
binding?.applyBtt?.setOnClickListener {
it.context.saveStyle(state)
applyStyleEvent.invoke(state)
//it.context.fromSaveToStyle(state)
activity?.popCurrentPage()
}
subtitle_text.setCues(
listOf(
Cue.Builder()
.setTextSize(
getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(),
Cue.TEXT_SIZE_TYPE_ABSOLUTE
)
.setText(subtitle_text.context.getString(R.string.subtitles_example_text))
.build()
binding?.subtitleText?.apply {
setCues(
listOf(
Cue.Builder()
.setTextSize(
getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(),
Cue.TEXT_SIZE_TYPE_ABSOLUTE
)
.setText(context.getString(R.string.subtitles_example_text))
.build()
)
)
)
}
}
}

View file

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
@ -37,8 +38,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.subtitle_settings.*
import kotlinx.android.synthetic.main.toast.view.*
import java.io.File
const val SUBTITLE_KEY = "subtitle_settings"
@ -184,10 +183,10 @@ class SubtitlesFragment : Fragment() {
}
private fun Context.updateState() {
subtitle_text?.setStyle(fromSaveToStyle(state))
val text = subtitle_text.context.getString(R.string.subtitles_example_text)
binding?.subtitleText?.setStyle(fromSaveToStyle(state))
val text = getString(R.string.subtitles_example_text)
val fixedText = if (state.upperCase) text.uppercase() else text
subtitle_text?.setCues(
binding?.subtitleText?.setCues(
listOf(
Cue.Builder()
.setTextSize(
@ -213,12 +212,21 @@ class SubtitlesFragment : Fragment() {
return if (color == Color.TRANSPARENT) Color.BLACK else color
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
var binding: SubtitleSettingsBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.subtitle_settings, container, false)
savedInstanceState: Bundle?
): View {
val localBinding = SubtitleSettingsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.subtitle_settings, container, false)
}
private lateinit var state: SaveCaptionStyle
@ -234,11 +242,11 @@ class SubtitlesFragment : Fragment() {
hide = arguments?.getBoolean("hide") ?: true
onColorSelectedEvent += ::onColorSelected
onDialogDismissedEvent += ::onDialogDismissed
subs_import_text?.text = getString(R.string.subs_import_text).format(
binding?.subsImportText?.text = getString(R.string.subs_import_text).format(
context?.getExternalFilesDir(null)?.absolutePath.toString() + "/Fonts"
)
context?.fixPaddingStatusbar(subs_root)
fixPaddingStatusbar(binding?.subsRoot)
state = getCurrentSavedStyle()
context?.updateState()
@ -264,317 +272,318 @@ class SubtitlesFragment : Fragment() {
this.setOnLongClickListener {
it.context.setColor(id, null)
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
}
binding?.apply {
subsTextColor.setup(0)
subsOutlineColor.setup(1)
subsBackgroundColor.setup(2)
subsWindowColor.setup(3)
subs_text_color.setup(0)
subs_outline_color.setup(1)
subs_background_color.setup(2)
subs_window_color.setup(3)
val dismissCallback = {
if (hide)
activity?.hideSystemUI()
}
subs_subtitle_elevation.setFocusableInTv()
subs_subtitle_elevation.setOnClickListener { textView ->
val suffix = "dp"
val elevationTypes = listOf(
Pair(0, textView.context.getString(R.string.none)),
Pair(10, "10$suffix"),
Pair(20, "20$suffix"),
Pair(30, "30$suffix"),
Pair(40, "40$suffix"),
Pair(50, "50$suffix"),
Pair(60, "60$suffix"),
Pair(70, "70$suffix"),
Pair(80, "80$suffix"),
Pair(90, "90$suffix"),
Pair(100, "100$suffix"),
)
//showBottomDialog
activity?.showDialog(
elevationTypes.map { it.second },
elevationTypes.map { it.first }.indexOf(state.elevation),
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
state.elevation = elevationTypes.map { it.first }[index]
textView.context.updateState()
val dismissCallback = {
if (hide)
activity?.hideSystemUI()
}
}
subs_subtitle_elevation.setOnLongClickListener {
state.elevation = DEF_SUBS_ELEVATION
it.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subsSubtitleElevation.setFocusableInTv()
subsSubtitleElevation.setOnClickListener { textView ->
val suffix = "dp"
val elevationTypes = listOf(
Pair(0, textView.context.getString(R.string.none)),
Pair(10, "10$suffix"),
Pair(20, "20$suffix"),
Pair(30, "30$suffix"),
Pair(40, "40$suffix"),
Pair(50, "50$suffix"),
Pair(60, "60$suffix"),
Pair(70, "70$suffix"),
Pair(80, "80$suffix"),
Pair(90, "90$suffix"),
Pair(100, "100$suffix"),
)
subs_edge_type.setFocusableInTv()
subs_edge_type.setOnClickListener { textView ->
val edgeTypes = listOf(
Pair(
CaptionStyleCompat.EDGE_TYPE_NONE,
textView.context.getString(R.string.subtitles_none)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_OUTLINE,
textView.context.getString(R.string.subtitles_outline)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_DEPRESSED,
textView.context.getString(R.string.subtitles_depressed)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW,
textView.context.getString(R.string.subtitles_shadow)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_RAISED,
textView.context.getString(R.string.subtitles_raised)
),
)
//showBottomDialog
activity?.showDialog(
edgeTypes.map { it.second },
edgeTypes.map { it.first }.indexOf(state.edgeType),
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
state.edgeType = edgeTypes.map { it.first }[index]
textView.context.updateState()
}
}
subs_edge_type.setOnLongClickListener {
state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE
it.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subs_font_size.setFocusableInTv()
subs_font_size.setOnClickListener { textView ->
val suffix = "sp"
val fontSizes = listOf(
Pair(null, textView.context.getString(R.string.normal)),
Pair(6f, "6$suffix"),
Pair(7f, "7$suffix"),
Pair(8f, "8$suffix"),
Pair(9f, "9$suffix"),
Pair(10f, "10$suffix"),
Pair(11f, "11$suffix"),
Pair(12f, "12$suffix"),
Pair(13f, "13$suffix"),
Pair(14f, "14$suffix"),
Pair(15f, "15$suffix"),
Pair(16f, "16$suffix"),
Pair(17f, "17$suffix"),
Pair(18f, "18$suffix"),
Pair(19f, "19$suffix"),
Pair(20f, "20$suffix"),
Pair(21f, "21$suffix"),
Pair(22f, "22$suffix"),
Pair(23f, "23$suffix"),
Pair(24f, "24$suffix"),
Pair(25f, "25$suffix"),
Pair(26f, "26$suffix"),
Pair(28f, "28$suffix"),
Pair(30f, "30$suffix"),
Pair(32f, "32$suffix"),
Pair(34f, "34$suffix"),
Pair(36f, "36$suffix"),
Pair(38f, "38$suffix"),
Pair(40f, "40$suffix"),
Pair(42f, "42$suffix"),
Pair(44f, "44$suffix"),
Pair(48f, "48$suffix"),
Pair(60f, "60$suffix"),
)
//showBottomDialog
activity?.showDialog(
fontSizes.map { it.second },
fontSizes.map { it.first }.indexOf(state.fixedTextSize),
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
state.fixedTextSize = fontSizes.map { it.first }[index]
//textView.context.updateState() // font size not changed
}
}
subtitles_remove_bloat?.isChecked = state.removeBloat
subtitles_remove_bloat?.setOnCheckedChangeListener { _, b ->
state.removeBloat = b
}
subtitles_uppercase?.isChecked = state.upperCase
subtitles_uppercase?.setOnCheckedChangeListener { _, b ->
state.upperCase = b
context?.updateState()
}
subtitles_remove_captions?.isChecked = state.removeCaptions
subtitles_remove_captions?.setOnCheckedChangeListener { _, b ->
state.removeCaptions = b
}
subs_font_size.setOnLongClickListener { _ ->
state.fixedTextSize = null
//textView.context.updateState() // font size not changed
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
//Fetch current value from preference
context?.let { ctx ->
subtitles_filter_sub_lang?.isChecked =
PreferenceManager.getDefaultSharedPreferences(ctx)
.getBoolean(getString(R.string.filter_sub_lang_key), false)
}
subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b ->
context?.let { ctx ->
PreferenceManager.getDefaultSharedPreferences(ctx)
.edit()
.putBoolean(getString(R.string.filter_sub_lang_key), b)
.apply()
}
}
subs_font.setFocusableInTv()
subs_font.setOnClickListener { textView ->
val fontTypes = listOf(
Pair(null, textView.context.getString(R.string.normal)),
Pair(R.font.trebuchet_ms, "Trebuchet MS"),
Pair(R.font.netflix_sans, "Netflix Sans"),
Pair(R.font.google_sans, "Google Sans"),
Pair(R.font.open_sans, "Open Sans"),
Pair(R.font.futura, "Futura"),
Pair(R.font.consola, "Consola"),
Pair(R.font.gotham, "Gotham"),
Pair(R.font.lucida_grande, "Lucida Grande"),
Pair(R.font.stix_general, "STIX General"),
Pair(R.font.times_new_roman, "Times New Roman"),
Pair(R.font.verdana, "Verdana"),
Pair(R.font.ubuntu_regular, "Ubuntu"),
Pair(R.font.comic_sans, "Comic Sans"),
Pair(R.font.poppins_regular, "Poppins"),
)
val savedFontTypes = textView.context.getSavedFonts()
val currentIndex =
savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath }
.let { index ->
if (index == -1)
fontTypes.indexOfFirst { it.first == state.typeface }
else index + fontTypes.size
}
//showBottomDialog
activity?.showDialog(
fontTypes.map { it.second } + savedFontTypes.map { it.name },
currentIndex,
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
if (index < fontTypes.size) {
state.typeface = fontTypes[index].first
state.typefaceFilePath = null
} else {
state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath
state.typeface = null
//showBottomDialog
activity?.showDialog(
elevationTypes.map { it.second },
elevationTypes.map { it.first }.indexOf(state.elevation),
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
state.elevation = elevationTypes.map { it.first }[index]
textView.context.updateState()
if (hide)
activity?.hideSystemUI()
}
}
subsSubtitleElevation.setOnLongClickListener {
state.elevation = DEF_SUBS_ELEVATION
it.context.updateState()
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subsEdgeType.setFocusableInTv()
subsEdgeType.setOnClickListener { textView ->
val edgeTypes = listOf(
Pair(
CaptionStyleCompat.EDGE_TYPE_NONE,
textView.context.getString(R.string.subtitles_none)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_OUTLINE,
textView.context.getString(R.string.subtitles_outline)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_DEPRESSED,
textView.context.getString(R.string.subtitles_depressed)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW,
textView.context.getString(R.string.subtitles_shadow)
),
Pair(
CaptionStyleCompat.EDGE_TYPE_RAISED,
textView.context.getString(R.string.subtitles_raised)
),
)
//showBottomDialog
activity?.showDialog(
edgeTypes.map { it.second },
edgeTypes.map { it.first }.indexOf(state.edgeType),
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
state.edgeType = edgeTypes.map { it.first }[index]
textView.context.updateState()
}
}
subsEdgeType.setOnLongClickListener {
state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE
it.context.updateState()
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subsFontSize.setFocusableInTv()
subsFontSize.setOnClickListener { textView ->
val suffix = "sp"
val fontSizes = listOf(
Pair(null, textView.context.getString(R.string.normal)),
Pair(6f, "6$suffix"),
Pair(7f, "7$suffix"),
Pair(8f, "8$suffix"),
Pair(9f, "9$suffix"),
Pair(10f, "10$suffix"),
Pair(11f, "11$suffix"),
Pair(12f, "12$suffix"),
Pair(13f, "13$suffix"),
Pair(14f, "14$suffix"),
Pair(15f, "15$suffix"),
Pair(16f, "16$suffix"),
Pair(17f, "17$suffix"),
Pair(18f, "18$suffix"),
Pair(19f, "19$suffix"),
Pair(20f, "20$suffix"),
Pair(21f, "21$suffix"),
Pair(22f, "22$suffix"),
Pair(23f, "23$suffix"),
Pair(24f, "24$suffix"),
Pair(25f, "25$suffix"),
Pair(26f, "26$suffix"),
Pair(28f, "28$suffix"),
Pair(30f, "30$suffix"),
Pair(32f, "32$suffix"),
Pair(34f, "34$suffix"),
Pair(36f, "36$suffix"),
Pair(38f, "38$suffix"),
Pair(40f, "40$suffix"),
Pair(42f, "42$suffix"),
Pair(44f, "44$suffix"),
Pair(48f, "48$suffix"),
Pair(60f, "60$suffix"),
)
//showBottomDialog
activity?.showDialog(
fontSizes.map { it.second },
fontSizes.map { it.first }.indexOf(state.fixedTextSize),
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
state.fixedTextSize = fontSizes.map { it.first }[index]
//textView.context.updateState() // font size not changed
}
}
subtitlesRemoveBloat.isChecked = state.removeBloat
subtitlesRemoveBloat.setOnCheckedChangeListener { _, b ->
state.removeBloat = b
}
subtitlesUppercase.isChecked = state.upperCase
subtitlesUppercase.setOnCheckedChangeListener { _, b ->
state.upperCase = b
context?.updateState()
}
subtitlesRemoveCaptions.isChecked = state.removeCaptions
subtitlesRemoveCaptions.setOnCheckedChangeListener { _, b ->
state.removeCaptions = b
}
subsFontSize.setOnLongClickListener { _ ->
state.fixedTextSize = null
//textView.context.updateState() // font size not changed
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
//Fetch current value from preference
context?.let { ctx ->
subtitlesFilterSubLang.isChecked =
PreferenceManager.getDefaultSharedPreferences(ctx)
.getBoolean(getString(R.string.filter_sub_lang_key), false)
}
subtitlesFilterSubLang.setOnCheckedChangeListener { _, b ->
context?.let { ctx ->
PreferenceManager.getDefaultSharedPreferences(ctx)
.edit()
.putBoolean(getString(R.string.filter_sub_lang_key), b)
.apply()
}
}
subsFont.setFocusableInTv()
subsFont.setOnClickListener { textView ->
val fontTypes = listOf(
Pair(null, textView.context.getString(R.string.normal)),
Pair(R.font.trebuchet_ms, "Trebuchet MS"),
Pair(R.font.netflix_sans, "Netflix Sans"),
Pair(R.font.google_sans, "Google Sans"),
Pair(R.font.open_sans, "Open Sans"),
Pair(R.font.futura, "Futura"),
Pair(R.font.consola, "Consola"),
Pair(R.font.gotham, "Gotham"),
Pair(R.font.lucida_grande, "Lucida Grande"),
Pair(R.font.stix_general, "STIX General"),
Pair(R.font.times_new_roman, "Times New Roman"),
Pair(R.font.verdana, "Verdana"),
Pair(R.font.ubuntu_regular, "Ubuntu"),
Pair(R.font.comic_sans, "Comic Sans"),
Pair(R.font.poppins_regular, "Poppins"),
)
val savedFontTypes = textView.context.getSavedFonts()
val currentIndex =
savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath }
.let { index ->
if (index == -1)
fontTypes.indexOfFirst { it.first == state.typeface }
else index + fontTypes.size
}
//showBottomDialog
activity?.showDialog(
fontTypes.map { it.second } + savedFontTypes.map { it.name },
currentIndex,
(textView as TextView).text.toString(),
false,
dismissCallback
) { index ->
if (index < fontTypes.size) {
state.typeface = fontTypes[index].first
state.typefaceFilePath = null
} else {
state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath
state.typeface = null
}
textView.context.updateState()
}
}
subsFont.setOnLongClickListener { textView ->
state.typeface = null
state.typefaceFilePath = null
textView.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
}
subs_font.setOnLongClickListener { textView ->
state.typeface = null
state.typefaceFilePath = null
textView.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subsAutoSelectLanguage.setFocusableInTv()
subsAutoSelectLanguage.setOnClickListener { textView ->
val langMap = arrayListOf(
SubtitleHelper.Language639(
textView.context.getString(R.string.none),
textView.context.getString(R.string.none),
"",
"",
"",
"",
""
),
)
langMap.addAll(SubtitleHelper.languages)
subs_auto_select_language.setFocusableInTv()
subs_auto_select_language.setOnClickListener { textView ->
val langMap = arrayListOf(
SubtitleHelper.Language639(
textView.context.getString(R.string.none),
textView.context.getString(R.string.none),
"",
"",
"",
"",
""
),
)
langMap.addAll(SubtitleHelper.languages)
val lang639_1 = langMap.map { it.ISO_639_1 }
activity?.showDialog(
langMap.map { it.languageName },
lang639_1.indexOf(getAutoSelectLanguageISO639_1()),
(textView as TextView).text.toString(),
true,
dismissCallback
) { index ->
setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
val lang639_1 = langMap.map { it.ISO_639_1 }
activity?.showDialog(
langMap.map { it.languageName },
lang639_1.indexOf(getAutoSelectLanguageISO639_1()),
(textView as TextView).text.toString(),
true,
dismissCallback
) { index ->
setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
}
}
}
subs_auto_select_language.setOnLongClickListener {
setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subs_download_languages.setFocusableInTv()
subs_download_languages.setOnClickListener { textView ->
val langMap = SubtitleHelper.languages
val lang639_1 = langMap.map { it.ISO_639_1 }
val keys = getDownloadSubsLanguageISO639_1()
val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 }
activity?.showMultiDialog(
langMap.map { it.languageName },
keyMap,
(textView as TextView).text.toString(),
dismissCallback
) { indexList ->
setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
subsAutoSelectLanguage.setOnLongClickListener {
setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
}
subs_download_languages.setOnLongClickListener {
setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
subsDownloadLanguages.setFocusableInTv()
subsDownloadLanguages.setOnClickListener { textView ->
val langMap = SubtitleHelper.languages
val lang639_1 = langMap.map { it.ISO_639_1 }
val keys = getDownloadSubsLanguageISO639_1()
val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 }
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
activity?.showMultiDialog(
langMap.map { it.languageName },
keyMap,
(textView as TextView).text.toString(),
dismissCallback
) { indexList ->
setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
}
}
cancel_btt.setOnClickListener {
activity?.popCurrentPage()
}
subsDownloadLanguages.setOnLongClickListener {
setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
apply_btt.setOnClickListener {
it.context.saveStyle(state)
applyStyleEvent.invoke(state)
it.context.fromSaveToStyle(state)
activity?.popCurrentPage()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
cancelBtt.setOnClickListener {
activity?.popCurrentPage()
}
applyBtt.setOnClickListener {
it.context.saveStyle(state)
applyStyleEvent.invoke(state)
it.context.fromSaveToStyle(state)
activity?.popCurrentPage()
}
}
}
}

View file

@ -20,6 +20,9 @@ import android.os.*
import android.provider.MediaStore
import android.text.Spanned
import android.util.Log
import android.view.View
import android.view.View.LAYOUT_DIRECTION_LTR
import android.view.View.LAYOUT_DIRECTION_RTL
import android.view.animation.DecelerateInterpolator
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
@ -33,6 +36,7 @@ import androidx.core.widget.ContentLoadingProgressBar
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -47,12 +51,14 @@ import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.wrappers.Wrappers
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.ui.WebviewFragment
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@ -87,6 +93,9 @@ object AppUtils {
return if (layoutManager == null || adapter == null) false else layoutManager.findLastCompletelyVisibleItemPosition() < adapter.itemCount - 7 // bit more than 1 to make it more seamless
}
fun View.isLtr() = this.layoutDirection == LAYOUT_DIRECTION_LTR
fun View.isRtl() = this.layoutDirection == LAYOUT_DIRECTION_RTL
fun BottomSheetDialog?.ownHide() {
this?.hide()
}
@ -198,7 +207,11 @@ object AppUtils {
animation.start()
}
fun Context.createNotificationChannel(channelId: String, channelName: String, description: String) {
fun Context.createNotificationChannel(
channelId: String,
channelName: String,
description: String
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel =
@ -288,6 +301,7 @@ object AppUtils {
// https://github.com/googlearchive/leanback-homescreen-channels/blob/master/app/src/main/java/com/google/android/tvhomescreenchannels/SampleTvProvider.java
@SuppressLint("RestrictedApi")
@Throws
@WorkerThread
suspend fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
@ -369,7 +383,6 @@ object AppUtils {
)
main {
showToast(
this@loadRepository,
getString(R.string.player_loaded_subtitles, repo.name),
Toast.LENGTH_LONG
)
@ -577,12 +590,29 @@ object AppUtils {
}
}
fun loadResult(
url: String,
apiName: String,
startAction: Int = 0,
startValue: Int = 0
) {
(activity as FragmentActivity?)?.loadResult(url, apiName, startAction, startValue)
}
fun FragmentActivity.loadResult(
url: String,
apiName: String,
startAction: Int = 0,
startValue: Int = 0
) {
try {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
Kitsu.isEnabled =
settingsManager.getBoolean(this.getString(R.string.show_kitsu_posters_key), true)
}catch (t : Throwable) {
logError(t)
}
this.runOnUiThread {
// viewModelStore.clear()
this.navigate(
@ -592,6 +622,14 @@ object AppUtils {
}
}
fun loadSearchResult(
card: SearchResponse,
startAction: Int = 0,
startValue: Int? = null,
) {
activity?.loadSearchResult(card, startAction, startValue)
}
fun Activity?.loadSearchResult(
card: SearchResponse,
startAction: Int = 0,
@ -776,12 +814,12 @@ object AppUtils {
return networkInfo.any {
conManager.getNetworkCapabilities(it)
?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
} &&
} &&
!networkInfo.any {
conManager.getNetworkCapabilities(it)
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
}
}
}
private fun Activity?.cacheClass(clazz: String?) {

View file

@ -149,7 +149,7 @@ object BackupUtils {
fun FragmentActivity.backup() {
try {
if (!checkWrite()) {
showToast(this, getString(R.string.backup_failed), Toast.LENGTH_LONG)
showToast(getString(R.string.backup_failed), Toast.LENGTH_LONG)
requestRW()
return
}
@ -201,7 +201,6 @@ object BackupUtils {
printStream.close()
showToast(
this,
R.string.backup_success,
Toast.LENGTH_LONG
)
@ -209,7 +208,6 @@ object BackupUtils {
logError(e)
try {
showToast(
this,
getString(R.string.backup_failed_error_format).format(e.toString()),
Toast.LENGTH_LONG
)
@ -243,7 +241,6 @@ object BackupUtils {
logError(e)
main { // smth can fail in .format
showToast(
activity,
getString(R.string.restore_failed_format).format(e.toString())
)
}
@ -270,7 +267,7 @@ object BackupUtils {
)
)
} catch (e: Exception) {
showToast(this, e.message)
showToast(e.message)
logError(e)
}
}

View file

@ -18,6 +18,8 @@ const val USER_PROVIDER_API = "user_custom_sites"
const val PREFERENCES_NAME = "rebuild_preference"
// TODO degelgate by value for get & set
object DataStore {
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()

View file

@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.utils
class Event<T> {
private val observers = mutableSetOf<(T) -> Unit>()
val size : Int get() = observers.size
operator fun plusAssign(observer: (T) -> Unit) {
observers.add(observer)
}

View file

@ -399,6 +399,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Moviehab(),
MoviehabNet(),
Jeniusplay(),
StreamoUpload(),
Gdriveplayerapi(),
Gdriveplayerapp(),

View file

@ -300,7 +300,7 @@ class InAppUpdater {
// Forcefully start any delayed installations
if (ApkInstaller.delayedInstaller?.startInstallation() == true) return@setPositiveButton
showToast(context, R.string.download_started, Toast.LENGTH_LONG)
showToast(R.string.download_started, Toast.LENGTH_LONG)
// Check if the setting hasn't been changed
if (settingsManager.getInt(
@ -335,7 +335,6 @@ class InAppUpdater {
if (!downloadUpdate(update.updateURL))
runOnUiThread {
showToast(
context,
R.string.download_failed,
Toast.LENGTH_LONG
)

View file

@ -2,19 +2,28 @@ package com.lagradost.cloudstream3.utils
import android.app.Activity
import android.app.Dialog
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.view.*
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.marginLeft
import androidx.core.view.marginRight
import androidx.core.view.marginTop
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.add_account_input.*
import kotlinx.android.synthetic.main.add_account_input.text1
import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.*
object SingleSelectionHelper {
fun Activity?.showOptionSelectStringRes(
@ -82,6 +91,7 @@ object SingleSelectionHelper {
}
fun Activity?.showDialog(
binding: BottomSelectionDialogBinding,
dialog: Dialog,
items: List<String>,
selectedIndex: List<Int>,
@ -95,39 +105,39 @@ object SingleSelectionHelper {
if (this == null) return
val realShowApply = showApply || isMultiSelect
val listView = dialog.listview1//.findViewById<ListView>(R.id.listview1)!!
val textView = dialog.text1//.findViewById<TextView>(R.id.text1)!!
val applyButton = dialog.apply_btt//.findViewById<TextView>(R.id.apply_btt)
val cancelButton = dialog.cancel_btt//findViewById<TextView>(R.id.cancel_btt)
val listView = binding.listview1//.findViewById<ListView>(R.id.listview1)!!
val textView = binding.text1//.findViewById<TextView>(R.id.text1)!!
val applyButton = binding.applyBtt//.findViewById<TextView>(R.id.apply_btt)
val cancelButton = binding.cancelBtt//findViewById<TextView>(R.id.cancel_btt)
val applyHolder =
dialog.apply_btt_holder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
binding.applyBttHolder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
applyHolder?.isVisible = realShowApply
applyHolder.isVisible = realShowApply
if (!realShowApply) {
val params = listView.layoutParams as LinearLayout.LayoutParams
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
listView.layoutParams = params
}
textView?.text = name
textView?.isGone = name.isBlank()
textView.text = name
textView.isGone = name.isBlank()
val arrayAdapter = ArrayAdapter<String>(this, itemLayout)
arrayAdapter.addAll(items)
listView?.adapter = arrayAdapter
listView.adapter = arrayAdapter
if (isMultiSelect) {
listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
} else {
listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
}
for (select in selectedIndex) {
listView?.setItemChecked(select, true)
listView.setItemChecked(select, true)
}
selectedIndex.minOrNull()?.let {
listView?.setSelection(it)
listView.setSelection(it)
}
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
@ -136,7 +146,7 @@ object SingleSelectionHelper {
dismissCallback.invoke()
}
listView?.setOnItemClickListener { _, _, which, _ ->
listView.setOnItemClickListener { _, _, which, _ ->
// lastSelectedIndex = which
if (realShowApply) {
if (!isMultiSelect) {
@ -148,7 +158,7 @@ object SingleSelectionHelper {
}
}
if (realShowApply) {
applyButton?.setOnClickListener {
applyButton.setOnClickListener {
val list = ArrayList<Int>()
for (index in 0 until listView.count) {
if (listView.checkedItemPositions[index])
@ -157,7 +167,7 @@ object SingleSelectionHelper {
callback.invoke(list)
dialog.dismissSafe(this)
}
cancelButton?.setOnClickListener {
cancelButton.setOnClickListener {
dialog.dismissSafe(this)
}
}
@ -213,13 +223,26 @@ object SingleSelectionHelper {
) {
if (this == null) return
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
LayoutInflater.from(this)
)
val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.bottom_selection_dialog)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback)
showDialog(
binding,
dialog,
items,
selectedIndex,
name,
showApply = true,
isMultiSelect = true,
callback,
dismissCallback
)
}
fun Activity?.showDialog(
@ -232,13 +255,19 @@ object SingleSelectionHelper {
) {
if (this == null) return
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
LayoutInflater.from(this)
)
val builder =
AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(R.layout.bottom_selection_dialog)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
showDialog(
binding,
dialog,
items,
listOf(selectedIndex),
@ -260,12 +289,18 @@ object SingleSelectionHelper {
callback: (Int) -> Unit,
) {
if (this == null) return
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
LayoutInflater.from(this)
)
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_selection_dialog)
builder.setContentView(binding.root)
builder.show()
showDialog(
binding,
builder,
items,
listOf(selectedIndex),
@ -285,13 +320,19 @@ object SingleSelectionHelper {
): BottomSheetDialog {
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_selection_dialog_direct)
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
LayoutInflater.from(this)
)
//builder.setContentView(R.layout.bottom_selection_dialog_direct)
builder.setContentView(binding.root)
builder.show()
showDialog(
binding,
builder,
items,
listOf(),
emptyList(),
name,
showApply = false,
isMultiSelect = false,

Some files were not shown because too many files have changed in this diff Show more