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 }} private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive" repository: "recloudstream/cloudstream-archive"
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 17
uses: actions/setup-java@v2 uses: actions/setup-java@v2
with: with:
java-version: '11' java-version: '17'
distribution: 'adopt' distribution: 'adopt'
- name: Grant execute permission for gradlew - name: Grant execute permission for gradlew
run: chmod +x gradlew run: chmod +x gradlew

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,30 @@
package com.lagradost.cloudstream3 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.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.SubtitleHelper
import com.lagradost.cloudstream3.utils.TestingUtils import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -8,16 +32,23 @@ import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *
* See [testing documentation](http://d.android.com/tools/testing). * 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) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
private fun getAllProviders(): List<MainAPI> { private fun getAllProviders(): Array<MainAPI> {
println("Providers: ${APIHolder.allProviders.size}") println("Providers: ${APIHolder.allProviders.size}")
return APIHolder.allProviders //.filter { !it.usesWebView } return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
} }
@Test @Test
@ -26,6 +57,73 @@ class ExampleInstrumentedTest {
println("Done providersExist") 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 @Test
@Throws(AssertionError::class) @Throws(AssertionError::class)
fun providerCorrectData() { fun providerCorrectData() {
@ -49,7 +147,7 @@ class ExampleInstrumentedTest {
@Test @Test
fun providerCorrectHomepage() { fun providerCorrectHomepage() {
runBlocking { runBlocking {
getAllProviders().amap { api -> getAllProviders().toList().amap { api ->
TestingUtils.testHomepage(api, ::println) TestingUtils.testHomepage(api, ::println)
} }
} }

View file

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

View file

@ -9,6 +9,7 @@ import android.content.res.Resources
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.view.View.NO_ID
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity 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.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv 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.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper 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.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.schabi.newpipe.extractor.NewPipe import org.schabi.newpipe.extractor.NewPipe
import java.lang.ref.WeakReference
import java.util.* import java.util.*
object CommonActivity { object CommonActivity {
private var _activity: WeakReference<Activity>? = null
var activity
get() = _activity?.get()
private set(value) {
_activity = WeakReference(value)
}
@MainThread @MainThread
fun Activity?.getCastSession(): CastSession? { fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession return (this as MainActivity?)?.mSessionManager?.currentCastSession
@ -56,6 +67,30 @@ object CommonActivity {
var currentToast: Toast? = null 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) { fun showToast(act: Activity?, text: UiText, duration: Int) {
if (act == null) return if (act == null) return
text.asStringNull(act)?.let { text.asStringNull(act)?.let {
@ -140,6 +175,7 @@ object CommonActivity {
fun init(act: ComponentActivity?) { fun init(act: ComponentActivity?) {
if (act == null) return if (act == null) return
activity = act
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //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 //https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode = canShowPipMode =
@ -260,27 +296,58 @@ object CommonActivity {
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW ) // 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( private fun getNextFocus(
act: Activity?, act: Activity?,
view: View?, view: View?,
direction: FocusDirection, direction: FocusDirection,
depth: Int = 0 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) { if (view == null || depth >= 10 || act == null) {
return null return null
} }
val nextId = when (direction) { var nextId = when (direction) {
FocusDirection.Left -> { FocusDirection.Start -> {
view.nextFocusLeftId if (view.isRtl())
view.nextFocusRightId
else
view.nextFocusLeftId
} }
FocusDirection.Up -> { FocusDirection.Up -> {
view.nextFocusUpId view.nextFocusUpId
} }
FocusDirection.Right -> { FocusDirection.End -> {
view.nextFocusRightId if (view.isRtl())
view.nextFocusLeftId
else
view.nextFocusRightId
} }
FocusDirection.Down -> { FocusDirection.Down -> {
@ -288,27 +355,35 @@ object CommonActivity {
} }
} }
return if (nextId != -1) { if (nextId == NO_ID) {
val next = act.findViewById<View?>(nextId) // if not specified then use forward id
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" ) nextId = view.nextFocusForwardId
// if view is still not found to next focus then return and let android decide
if (next?.isShown == false) { if (nextId == NO_ID) return null
getNextFocus(act, next, direction, depth + 1)
} else {
if (depth == 0) {
null
} else {
nextId
}
}
} else {
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 { private enum class FocusDirection {
Left, Start,
Right, End,
Up, Up,
Down, Down,
} }
@ -407,67 +482,64 @@ object CommonActivity {
//} //}
} }
/** overrides focus and custom key events */
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? { fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
if (act == null) return null if (act == null) return null
val currentFocus = act.currentFocus
event?.keyCode?.let { keyCode -> event?.keyCode?.let { keyCode ->
when (event.action) { if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
KeyEvent.ACTION_DOWN -> { val nextView = when (keyCode) {
if (act.currentFocus != null) { KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
val next = when (keyCode) { act,
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus( currentFocus,
act, FocusDirection.Start
act.currentFocus, )
FocusDirection.Left
)
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus( KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
act, act,
act.currentFocus, currentFocus,
FocusDirection.Right FocusDirection.End
) )
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus( KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
act, act,
act.currentFocus, currentFocus,
FocusDirection.Up FocusDirection.Up
) )
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus( KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
act, act,
act.currentFocus, currentFocus,
FocusDirection.Down FocusDirection.Down
) )
else -> null 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
//)
}
} }
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) { if (keyEventListener?.invoke(Pair(event, false)) == true) {
return true return true
} }

View file

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

View file

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3 package com.lagradost.cloudstream3
import android.animation.ValueAnimator
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -14,13 +15,17 @@ import android.view.KeyEvent
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider 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.Session
import com.google.android.gms.cast.framework.SessionManager import com.google.android.gms.cast.framework.SessionManager
import com.google.android.gms.cast.framework.SessionManagerListener 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.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar 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.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.mvvm.Resource 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.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observeNullable 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.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString 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.appStringRepo
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch 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.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText 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.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings 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.ApkInstaller
import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable 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.isNetworkAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus 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.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main 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.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW 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_PROVIDER_API
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser 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.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import java.io.File import java.io.File
import java.lang.ref.WeakReference
import java.net.URI import java.net.URI
import java.net.URLDecoder import java.net.URLDecoder
import java.nio.charset.Charset import java.nio.charset.Charset
import kotlin.math.absoluteValue
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -334,7 +335,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this@with.runOnUiThread { this@with.runOnUiThread {
try { try {
showToast( showToast(
this@with,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format( getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name api.name
) )
@ -362,8 +362,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// Use both navigation views to support both layouts. // Use both navigation views to support both layouts.
// It might be better to use the QuickSearch. // It might be better to use the QuickSearch.
nav_view?.selectedItemId = R.id.navigation_search activity?.findViewById<BottomNavigationView>(R.id.nav_view)?.selectedItemId =
nav_rail_view?.selectedItemId = R.id.navigation_search R.id.navigation_search
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringPlayer) { } else if (safeURI(str)?.scheme == appStringPlayer) {
val uri = Uri.parse(str) val uri = Uri.parse(str)
val name = uri.getQueryParameter("name") val name = uri.getQueryParameter("name")
@ -396,10 +398,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.navigate(R.id.navigation_downloads) this.navigate(R.id.navigation_downloads)
return true return true
} else { } else {
for (api in apis) { synchronized(apis) {
if (str.startsWith(api.mainUrl)) { for (api in apis) {
loadResult(str, api.name) if (str.startsWith(api.mainUrl)) {
return true loadResult(str, api.name)
return true
}
} }
} }
} }
@ -440,7 +444,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.hideKeyboard() this.hideKeyboard()
// Fucks up anime info layout since that has its own layout // Fucks up anime info layout since that has its own layout
cast_mini_controller_holder?.isVisible = binding?.castMiniControllerHolder?.isVisible =
!listOf( !listOf(
R.id.navigation_results_phone, R.id.navigation_results_phone,
R.id.navigation_results_tv, R.id.navigation_results_tv,
@ -476,15 +480,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_player, R.id.navigation_player,
).contains(destination.id) ).contains(destination.id)
nav_host_fragment?.apply { binding?.navHostFragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams 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 layoutParams = params
} }
@ -494,21 +510,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
Configuration.ORIENTATION_PORTRAIT -> { Configuration.ORIENTATION_PORTRAIT -> {
false isTvSettings()
} }
else -> { else -> {
false false
} }
} }
binding?.apply {
navView.isVisible = isNavVisible && !landscape
navRailView.isVisible = isNavVisible && landscape
nav_view?.isVisible = isNavVisible && !landscape // Hide library on TV since it is not supported yet :(
nav_rail_view?.isVisible = isNavVisible && landscape val isTrueTv = isTrueTvSettings()
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
// Hide library on TV since it is not supported yet :( navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
val isTrueTv = isTrueTvSettings() }
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
} }
//private var mCastSession: CastSession? = null //private var mCastSession: CastSession? = null
@ -576,11 +593,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
override fun dispatchKeyEvent(event: KeyEvent?): Boolean { override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
CommonActivity.dispatchKeyEvent(this, event)?.let { val start = System.currentTimeMillis()
return it 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) return super.dispatchKeyEvent(event)
} }
@ -685,27 +714,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
private fun onAllPluginsLoaded(success: Boolean = false) { private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe { ioSafe {
pluginsLock.withLock { pluginsLock.withLock {
// Load cloned sites after plugins have been loaded since clones depend on plugins. synchronized(allProviders) {
try { // Load cloned sites after plugins have been loaded since clones depend on plugins.
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list -> try {
list.forEach { custom -> getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass } list.forEach { custom ->
?.let { allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
allProviders.add(it.javaClass.newInstance().apply { ?.let {
name = custom.name allProviders.add(it.javaClass.newInstance().apply {
lang = custom.lang name = custom.name
mainUrl = custom.url.trimEnd('/') lang = custom.lang
canBeOverridden = false 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() { private fun hidePreviewPopupDialog() {
viewModel.clear()
bottomPreviewPopup.dismissSafe(this) bottomPreviewPopup.dismissSafe(this)
bottomPreviewPopup = null
bottomPreviewBinding = null
} }
var bottomPreviewPopup: BottomSheetDialog? = null private var bottomPreviewPopup: BottomSheetDialog? = null
private fun showPreviewPopupDialog(): BottomSheetDialog { private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null
val ret = (bottomPreviewPopup ?: run { private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding {
val ret = (bottomPreviewBinding ?: run {
val builder = val builder =
BottomSheetDialog(this) 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 { builder.setOnDismissListener {
bottomPreviewPopup = null bottomPreviewPopup = null
bottomPreviewBinding = null
viewModel.clear() viewModel.clear()
} }
builder.setCanceledOnTouchOutside(true) builder.setCanceledOnTouchOutside(true)
builder.show() builder.show()
builder bottomPreviewPopup = builder
binding
}) })
bottomPreviewPopup = ret
return 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?) { override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this) app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -767,16 +987,48 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (isCastApiAvailable()) { if (isCastApiAvailable()) {
mSessionManager = CastContext.getSharedInstance(this).sessionManager mSessionManager = CastContext.getSharedInstance(this).sessionManager
} }
} catch (e: Exception) { } catch (t: Throwable) {
logError(e) logError(t)
} }
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN) window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
updateTv() updateTv()
if (isTvSettings()) {
setContentView(R.layout.activity_main_tv) // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
} else { try {
setContentView(R.layout.activity_main) 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()) changeStatusBarState(isEmulatorSettings())
@ -807,7 +1059,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (PluginManager.checkSafeModeFile()) { if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall { normalSafeApiCall {
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG) showToast(R.string.safe_mode_file, Toast.LENGTH_LONG)
} }
} else if (lastError == null) { } else if (lastError == null) {
ioSafe { ioSafe {
@ -861,43 +1113,44 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
observeNullable(viewModel.page) { resource -> observeNullable(viewModel.page) { resource ->
if (resource == null) { if (resource == null) {
bottomPreviewPopup.dismissSafe(this) hidePreviewPopupDialog()
return@observeNullable return@observeNullable
} }
when (resource) { when (resource) {
is Resource.Failure -> { is Resource.Failure -> {
showToast(this, R.string.error) showToast(R.string.error)
viewModel.clear()
hidePreviewPopupDialog() hidePreviewPopupDialog()
} }
is Resource.Loading -> { is Resource.Loading -> {
showPreviewPopupDialog().apply { showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = true resultviewPreviewLoading.isVisible = true
resultview_preview_result?.isVisible = false resultviewPreviewResult.isVisible = false
resultview_preview_loading_shimmer?.startShimmer() resultviewPreviewLoadingShimmer.startShimmer()
} }
} }
is Resource.Success -> { is Resource.Success -> {
val d = resource.value val d = resource.value
showPreviewPopupDialog().apply { showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = false resultviewPreviewLoading.isVisible = false
resultview_preview_result?.isVisible = true resultviewPreviewResult.isVisible = true
resultview_preview_loading_shimmer?.stopShimmer() resultviewPreviewLoadingShimmer.stopShimmer()
resultview_preview_title?.text = d.title resultviewPreviewTitle.text = d.title
resultview_preview_meta_type.setText(d.typeText) resultviewPreviewMetaType.setText(d.typeText)
resultview_preview_meta_year.setText(d.yearText) resultviewPreviewMetaYear.setText(d.yearText)
resultview_preview_meta_duration.setText(d.durationText) resultviewPreviewMetaDuration.setText(d.durationText)
resultview_preview_meta_rating.setText(d.ratingText) resultviewPreviewMetaRating.setText(d.ratingText)
resultview_preview_description?.setText(d.plotText) resultviewPreviewDescription.setText(d.plotText)
resultview_preview_poster?.setImage( resultviewPreviewPoster.setImage(
d.posterImage ?: d.posterBackgroundImage d.posterImage ?: d.posterBackgroundImage
) )
resultview_preview_poster?.setOnClickListener { resultviewPreviewPoster.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH) //viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
val value = viewModel.watchStatus.value ?: WatchType.NONE val value = viewModel.watchStatus.value ?: WatchType.NONE
@ -908,12 +1161,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
showApply = false, showApply = false,
{}) { {}) {
viewModel.updateWatchStatus(WatchType.values()[it]) viewModel.updateWatchStatus(WatchType.values()[it])
bookmarksUpdatedEvent(true)
} }
} }
if (!isTvSettings()) // dont want this clickable on tv layout if (!isTvSettings()) // dont want this clickable on tv layout
resultview_preview_description?.setOnClickListener { view -> resultviewPreviewDescription.setOnClickListener { view ->
view.context?.let { ctx -> view.context?.let { ctx ->
val builder: AlertDialog.Builder = val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom) AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
@ -923,7 +1175,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
resultview_preview_more_info?.setOnClickListener { resultviewPreviewMoreInfo.setOnClickListener {
viewModel.clear()
hidePreviewPopupDialog() hidePreviewPopupDialog()
lastPopup?.let { lastPopup?.let {
loadSearchResult(it) loadSearchResult(it)
@ -964,7 +1217,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
ioSafe { ioSafe {
initAll() initAll()
// No duplicates (which can happen by registerMainAPI) // 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) // val navView: BottomNavigationView = findViewById(R.id.nav_view)
@ -977,6 +1232,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? -> navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
// Intercept search and add a query // Intercept search and add a query
updateNavBar(navDestination)
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) { if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
bundle?.apply { bundle?.apply {
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery) this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
@ -995,29 +1251,47 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
.setPopExitAnim(R.anim.nav_pop_exit) .setPopExitAnim(R.anim.nav_pop_exit)
.setPopUpTo(navController.graph.startDestination, false) .setPopUpTo(navController.graph.startDestination, false)
.build()*/ .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( binding?.navRailView?.apply {
item, itemRippleColor = rippleColor
navController itemActiveIndicatorColor = rippleColor
) setupWithNavController(navController)
} if (isTvSettings()) {
nav_view?.setOnItemSelectedListener { item -> background?.alpha = 200
onNavDestinationSelected( } else {
item, background?.alpha = 255
navController }
)
} setOnItemSelectedListener { item ->
navController.addOnDestinationChangedListener { _, destination, _ -> onNavDestinationSelected(
updateNavBar(destination) 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() loadCache()
@ -1040,17 +1314,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
true 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()) { if (!checkWrite()) {
requestRW() requestRW()
if (checkWrite()) return 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 // THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION
//if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) { //if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) {
@ -1117,14 +1386,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n" var providersAndroidManifestString = "Current androidmanifest should be:\n"
for (api in allProviders) { synchronized(allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${ for (api in allProviders) {
api.mainUrl.removePrefix( providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
"https://" api.mainUrl.removePrefix(
) "https://"
}\" android:pathPrefix=\"/\"/>\n" )
}\" android:pathPrefix=\"/\"/>\n"
}
} }
println(providersAndroidManifestString) 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, "") return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
} }
private val validApis by lazy { private val validApis
apis.filter { it.lang == this.lang && it::class.java != this::class.java } get() =
//.distinctBy { it.uniqueId } synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
} //.distinctBy { it.uniqueId }
data class CrossMetaData( data class CrossMetaData(
@JsonProperty("isSuccess") val isSuccess: Boolean, @JsonProperty("isSuccess") val isSuccess: Boolean,
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
override suspend fun load(url: String): LoadResponse? { override suspend fun load(url: String): LoadResponse? {
val base = super.load(url)?.apply { 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) val matchName = filterName(this.name)
when (this) { when (this) {
is MovieLoadResponse -> { is MovieLoadResponse -> {
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
this.dataUrl = this.dataUrl =
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson() CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
} }
else -> { else -> {
throw ErrorLoadingException("Nothing besides movies are implemented for this provider") throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
} }

View file

@ -25,13 +25,16 @@ class MultiAnimeProvider : MainAPI() {
} }
} }
private val validApis by lazy { private val validApis
APIHolder.apis.filter { get() =
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( synchronized(APIHolder.apis) {
TvType.Anime APIHolder.apis.filter {
) it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
} TvType.Anime
} )
}
}
private fun filterName(name: String): String { private fun filterName(name: String): String {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "") 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) } 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> { sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>() data class Success<out T>(val value: T) : Resource<T>()
data class Failure( data class Failure(
@ -155,6 +129,70 @@ fun CoroutineScope.launchSafe(
return this.launch(context, start, obj) 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( suspend fun <T> safeApiCall(
apiCall: suspend () -> T, apiCall: suspend () -> T,
): Resource<T> { ): Resource<T> {
@ -163,60 +201,7 @@ suspend fun <T> safeApiCall(
Resource.Success(apiCall.invoke()) Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
logError(throwable) logError(throwable)
when (throwable) { throwAbleToResource(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)
}
} }
} }
} }

View file

@ -36,7 +36,9 @@ abstract class Plugin {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
element.sourcePlugin = this.__filename element.sourcePlugin = this.__filename
// Race condition causing which would case duplicates if not for distinctBy // 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) APIHolder.addPluginMapping(element)
} }
@ -51,10 +53,14 @@ abstract class Plugin {
} }
class Manifest { class Manifest {
@JsonProperty("name") var name: String? = null @JsonProperty("name")
@JsonProperty("pluginClassName") var pluginClassName: String? = null var name: String? = null
@JsonProperty("version") var version: Int? = null @JsonProperty("pluginClassName")
@JsonProperty("requiresResources") var requiresResources: Boolean = false 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> = private val classLoaders: MutableMap<PathClassLoader, Plugin> =
HashMap<PathClassLoader, Plugin>() HashMap<PathClassLoader, Plugin>()
private var loadedLocalPlugins = false var loadedLocalPlugins = false
private set
private val gson = Gson() private val gson = Gson()
private suspend fun maybeLoadPlugin(context: Context, file: File) { private suspend fun maybeLoadPlugin(context: Context, file: File) {
@ -531,10 +532,14 @@ object PluginManager {
} }
// remove all registered apis // remove all registered apis
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { synchronized(APIHolder.apis) {
removePluginMapping(it) 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 } extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
classLoaders.values.removeIf { v -> v == plugin } 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, parent: RecyclerView,
state: RecyclerView.State, state: RecyclerView.State,
child: View, child: View,
@ -32,13 +32,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
): Boolean { ): Boolean {
// android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams // android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams
return try { return try {
val pos = maxOf(0, getPosition(focused!!) - 2) if(focused != null) {
parent.scrollToPosition(pos) // 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) super.onRequestChildFocus(parent, state, child, focused)
} catch (e: Exception) { } catch (e: Exception) {
false false
} }
} }*/
// Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d // Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d
override fun onInterceptFocusSearch(focused: View, direction: Int): View? { override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
@ -65,8 +69,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
val spanCount = this.spanCount val spanCount = this.spanCount
val orientation = this.orientation 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) { if (orientation == VERTICAL) {
when (direction) { when (correctDirection) {
View.FOCUS_DOWN -> { View.FOCUS_DOWN -> {
return spanCount return spanCount
} }
@ -81,7 +94,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
} }
} }
} else if (orientation == HORIZONTAL) { } else if (orientation == HORIZONTAL) {
when (direction) { when (correctDirection) {
View.FOCUS_DOWN -> { View.FOCUS_DOWN -> {
return 1 return 1
} }

View file

@ -16,14 +16,16 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import kotlinx.android.synthetic.main.activity_easter_egg_monke.* import com.lagradost.cloudstream3.databinding.ActivityEasterEggMonkeBinding
import java.util.*
class EasterEggMonke : AppCompatActivity() { class EasterEggMonke : AppCompatActivity() {
lateinit var binding : ActivityEasterEggMonkeBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_easter_egg_monke)
binding = ActivityEasterEggMonkeBinding.inflate(layoutInflater)
setContentView(binding.root)
val handler = Handler(mainLooper) val handler = Handler(mainLooper)
lateinit var runnable: Runnable lateinit var runnable: Runnable
@ -32,15 +34,14 @@ class EasterEggMonke : AppCompatActivity() {
handler.postDelayed(runnable, 300) handler.postDelayed(runnable, 300)
} }
handler.postDelayed(runnable, 1000) handler.postDelayed(runnable, 1000)
} }
private fun shower() { private fun shower() {
val containerW = frame.width val containerW = binding.frame.width
val containerH = frame.height val containerH = binding.frame.height
var starW: Float = monke.width.toFloat() var starW: Float = binding.monke.width.toFloat()
var starH: Float = monke.height.toFloat() var starH: Float = binding.monke.height.toFloat()
val newStar = AppCompatImageView(this) val newStar = AppCompatImageView(this)
val idx = (monkeys.size * Math.random()).toInt() val idx = (monkeys.size * Math.random()).toInt()
@ -48,7 +49,7 @@ class EasterEggMonke : AppCompatActivity() {
newStar.isVisible = true newStar.isVisible = true
newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, newStar.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
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.scaleX = Math.random().toFloat() * 1.5f + newStar.scaleX
newStar.scaleY = newStar.scaleX newStar.scaleY = newStar.scaleX
@ -70,7 +71,7 @@ class EasterEggMonke : AppCompatActivity() {
set.addListener(object : AnimatorListenerAdapter() { set.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) { 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.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding
import com.lagradost.cloudstream3.network.WebViewResolver import com.lagradost.cloudstream3.network.WebViewResolver
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import kotlinx.android.synthetic.main.fragment_webview.*
class WebviewFragment : Fragment() { class WebviewFragment : Fragment() {
var binding: FragmentWebviewBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val url = arguments?.getString(WEBVIEW_URL) ?: "".also { val url = arguments?.getString(WEBVIEW_URL) ?: "".also {
findNavController().popBackStack() findNavController().popBackStack()
} }
web_view.webViewClient = object : WebViewClient() { binding?.webView?.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading( override fun shouldOverrideUrlLoading(
view: WebView?, view: WebView?,
request: WebResourceRequest? request: WebResourceRequest?
@ -40,24 +43,28 @@ class WebviewFragment : Fragment() {
return super.shouldOverrideUrlLoading(view, request) return super.shouldOverrideUrlLoading(view, request)
} }
} }
binding?.webView?.apply {
WebViewResolver.webViewUserAgent = settings.userAgentString
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString addJavascriptInterface(RepoApi(activity), "RepoApi")
settings.javaScriptEnabled = true
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi") settings.userAgentString = USER_AGENT
web_view.settings.javaScriptEnabled = true settings.domStorageEnabled = true
web_view.settings.userAgentString = USER_AGENT
web_view.settings.domStorageEnabled = true
// WebView.setWebContentsDebuggingEnabled(true) // WebView.setWebContentsDebuggingEnabled(true)
web_view.loadUrl(url) loadUrl(url)
}
} }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
val localBinding = FragmentWebviewBinding.inflate(inflater, container, false)
binding = localBinding
// Inflate the layout for this fragment // 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 { companion object {
@ -70,7 +77,7 @@ class WebviewFragment : Fragment() {
private class RepoApi(val activity: FragmentActivity?) { private class RepoApi(val activity: FragmentActivity?) {
@JavascriptInterface @JavascriptInterface
fun installRepo(repoUrl: String) { fun installRepo(repoUrl: String) {
activity?.loadRepository(repoUrl) activity?.loadRepository(repoUrl)
} }
} }

View file

@ -5,6 +5,7 @@ import android.content.DialogInterface
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -19,7 +20,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
object DownloadButtonSetup { object DownloadButtonSetup {
fun handleDownloadClick(activity: Activity?, click: DownloadClickEvent) { fun handleDownloadClick(click: DownloadClickEvent) {
val id = click.data.id val id = click.data.id
if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return if (click.data !is VideoDownloadHelper.DownloadEpisodeCached) return
when (click.action) { when (click.action) {
@ -89,9 +90,9 @@ object DownloadButtonSetup {
)?.fileLength )?.fileLength
?: 0 ?: 0
if (length > 0) { if (length > 0) {
showToast(act, R.string.delete, Toast.LENGTH_LONG) showToast(R.string.delete, Toast.LENGTH_LONG)
} else { } 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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 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.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.VideoDownloadHelper 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_PLAY_FILE = 0
const val DOWNLOAD_ACTION_DELETE_FILE = 1 const val DOWNLOAD_ACTION_DELETE_FILE = 1
@ -36,39 +30,9 @@ class DownloadChildAdapter(
private val clickCallback: (DownloadClickEvent) -> Unit, private val clickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DownloadChildViewHolder( return DownloadChildViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.download_child_episode, parent, false), DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false),
clickCallback clickCallback
) )
} }
@ -77,7 +41,6 @@ class DownloadChildAdapter(
when (holder) { when (holder) {
is DownloadChildViewHolder -> { is DownloadChildViewHolder -> {
holder.bind(cardList[position]) holder.bind(cardList[position])
mBoundViewHolders.add(holder)
} }
} }
} }
@ -88,66 +51,44 @@ class DownloadChildAdapter(
class DownloadChildViewHolder class DownloadChildViewHolder
constructor( constructor(
itemView: View, val binding: DownloadChildEpisodeBinding,
private val clickCallback: (DownloadClickEvent) -> Unit, private val clickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { ) : RecyclerView.ViewHolder(binding.root) {
override var downloadButton = EasyDownloadButton()
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 extraInfo: TextView = itemView.download_child_episode_text_extra
private val holder: CardView = itemView.download_child_episode_holder private val holder: CardView = itemView.download_child_episode_holder
private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress
private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded 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) { fun bind(card: VisualDownloadChildCached) {
localCard = card
val d = card.data val d = card.data
val posDur = getViewPos(d.id) val posDur = getViewPos(d.id)
if (posDur != null) { binding.downloadChildEpisodeProgress.apply {
val visualPos = posDur.fixVisual() if (posDur != null) {
progressBar.max = (visualPos.duration / 1000).toInt() val visualPos = posDur.fixVisual()
progressBar.progress = (visualPos.position / 1000).toInt() max = (visualPos.duration / 1000).toInt()
progressBar.visibility = View.VISIBLE progress = (visualPos.position / 1000).toInt()
} else { visibility = View.VISIBLE
progressBar.visibility = View.GONE } else {
visibility = View.GONE
}
} }
title.text = title.context.getNameFull(d.name, d.episode, d.season) binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback)
title.isSelected = true // is needed for text repeating
downloadButton.setUpButton( binding.downloadChildEpisodeText.apply {
card.currentBytes, text = context.getNameFull(d.name, d.episode, d.season)
card.totalBytes, isSelected = true // is needed for text repeating
progressBarDownload, }
downloadImage,
extraInfo,
card.data,
clickCallback
)
holder.setOnClickListener {
binding.downloadChildEpisodeHolder.setOnClickListener {
clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d)) 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.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey 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.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import kotlinx.android.synthetic.main.fragment_child_downloads.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class DownloadChildFragment : Fragment() { class DownloadChildFragment : Fragment() {
companion object { companion object {
fun newInstance(headerName: String, folder: String) : Bundle { fun newInstance(headerName: String, folder: String): Bundle {
return Bundle().apply { return Bundle().apply {
putString("folder", folder) putString("folder", folder)
putString("name", headerName) putString("name", headerName)
@ -30,13 +30,20 @@ class DownloadChildFragment : Fragment() {
} }
override fun onDestroyView() { override fun onDestroyView() {
(download_child_list?.adapter as DownloadChildAdapter?)?.killAdapter()
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it } downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
binding = null
super.onDestroyView() super.onDestroyView()
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { var binding: FragmentChildDownloadsBinding? = null
return inflater.inflate(R.layout.fragment_child_downloads, container, false) 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 { private fun updateList(folder: String) = main {
@ -50,14 +57,15 @@ class DownloadChildFragment : Fragment() {
?: return@mapNotNull null ?: return@mapNotNull null
VisualDownloadChildCached(info.fileLength, info.totalBytes, it) 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()) { if (eps.isEmpty()) {
activity?.onBackPressed() activity?.onBackPressed()
return@main return@main
} }
(download_child_list?.adapter as DownloadChildAdapter? ?: return@main).cardList = eps (binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList =
download_child_list?.adapter?.notifyDataSetChanged() eps
binding?.downloadChildList?.adapter?.notifyDataSetChanged()
} }
} }
@ -72,23 +80,26 @@ class DownloadChildFragment : Fragment() {
activity?.onBackPressed() // TODO FIX activity?.onBackPressed() // TODO FIX
return return
} }
context?.fixPaddingStatusbar(download_child_root) fixPaddingStatusbar(binding?.downloadChildRoot)
download_child_toolbar.title = name binding?.downloadChildToolbar?.apply {
download_child_toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) title = name
download_child_toolbar.setNavigationOnClickListener { setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
activity?.onBackPressed() setNavigationOnClickListener {
activity?.onBackPressed()
}
} }
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
DownloadChildAdapter( DownloadChildAdapter(
ArrayList(), ArrayList(),
) { click -> ) { click ->
handleDownloadClick(activity, click) handleDownloadClick(click)
} }
downloadDeleteEventListener = { id: Int -> 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 != null) {
if (list.any { it.data.id == id }) { if (list.any { it.data.id == id }) {
updateList(folder) updateList(folder)
@ -98,8 +109,8 @@ class DownloadChildFragment : Fragment() {
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
download_child_list.adapter = adapter binding?.downloadChildList?.adapter = adapter
download_child_list.layoutManager = GridLayoutManager(context, 1) binding?.downloadChildList?.layoutManager = GridLayoutManager(context, 1)
updateList(folder) 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.UIHelper.navigate
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager 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 android.text.format.Formatter.formatShortFileSize
import androidx.core.widget.doOnTextChanged 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.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.player.BasicLink
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
@ -60,8 +60,8 @@ class DownloadFragment : Fragment() {
private fun setList(list: List<VisualDownloadHeaderCached>) { private fun setList(list: List<VisualDownloadHeaderCached>) {
main { main {
(download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list
download_list?.adapter?.notifyDataSetChanged() binding?.downloadList?.adapter?.notifyDataSetChanged()
} }
} }
@ -70,10 +70,12 @@ class DownloadFragment : Fragment() {
VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!! VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!!
downloadDeleteEventListener = null downloadDeleteEventListener = null
} }
(download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter() binding = null
super.onDestroyView() super.onDestroyView()
} }
var binding : FragmentDownloadsBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -82,7 +84,9 @@ class DownloadFragment : Fragment() {
downloadsViewModel = downloadsViewModel =
ViewModelProvider(this)[DownloadViewModel::class.java] 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 private var downloadDeleteEventListener: ((Int) -> Unit)? = null
@ -92,36 +96,40 @@ class DownloadFragment : Fragment() {
hideKeyboard() hideKeyboard()
observe(downloadsViewModel.noDownloadsText) { observe(downloadsViewModel.noDownloadsText) {
text_no_downloads.text = it binding?.textNoDownloads?.text = it
} }
observe(downloadsViewModel.headerCards) { observe(downloadsViewModel.headerCards) {
setList(it) setList(it)
download_loading.isVisible = false binding?.downloadLoading?.isVisible = false
} }
observe(downloadsViewModel.availableBytes) { observe(downloadsViewModel.availableBytes) {
download_free_txt?.text = binding?.downloadFreeTxt?.text =
getString(R.string.storage_size_format).format( getString(R.string.storage_size_format).format(
getString(R.string.free_storage), getString(R.string.free_storage),
formatShortFileSize(view.context, it) formatShortFileSize(view.context, it)
) )
download_free?.setLayoutWidth(it) binding?.downloadFree?.setLayoutWidth(it)
} }
observe(downloadsViewModel.usedBytes) { observe(downloadsViewModel.usedBytes) {
download_used_txt?.text = binding?.apply {
getString(R.string.storage_size_format).format( downloadUsedTxt.text =
getString(R.string.used_storage), getString(R.string.storage_size_format).format(
formatShortFileSize(view.context, it) getString(R.string.used_storage),
) formatShortFileSize(view.context, it)
download_used?.setLayoutWidth(it) )
download_storage_appbar?.isVisible = it > 0 downloadUsed.setLayoutWidth(it)
downloadStorageAppbar.isVisible = it > 0
}
} }
observe(downloadsViewModel.downloadBytes) { observe(downloadsViewModel.downloadBytes) {
download_app_txt?.text = binding?.apply {
getString(R.string.storage_size_format).format( downloadAppTxt.text =
getString(R.string.app_storage), getString(R.string.storage_size_format).format(
formatShortFileSize(view.context, it) getString(R.string.app_storage),
) formatShortFileSize(view.context, it)
download_app?.setLayoutWidth(it) )
downloadApp.setLayoutWidth(it)
}
} }
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
@ -154,7 +162,7 @@ class DownloadFragment : Fragment() {
}, },
{ downloadClickEvent -> { downloadClickEvent ->
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
handleDownloadClick(activity, downloadClickEvent) handleDownloadClick(downloadClickEvent)
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) { if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
context?.let { ctx -> context?.let { ctx ->
downloadsViewModel.updateList(ctx) downloadsViewModel.updateList(ctx)
@ -164,7 +172,7 @@ class DownloadFragment : Fragment() {
) )
downloadDeleteEventListener = { id -> downloadDeleteEventListener = { id ->
val list = (download_list?.adapter as DownloadHeaderAdapter?)?.cardList val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList
if (list != null) { if (list != null) {
if (list.any { it.data.id == id }) { if (list.any { it.data.id == id }) {
context?.let { ctx -> context?.let { ctx ->
@ -177,31 +185,36 @@ class DownloadFragment : Fragment() {
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it } downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
download_list?.adapter = adapter binding?.downloadList?.apply {
download_list?.layoutManager = GridLayoutManager(context, 1) this.adapter = adapter
layoutManager = GridLayoutManager(context, 1)
}
// Should be visible in emulator layout // Should be visible in emulator layout
download_stream_button?.isGone = isTrueTvSettings() binding?.downloadStreamButton?.isGone = isTrueTvSettings()
download_stream_button?.setOnClickListener { binding?.downloadStreamButton?.setOnClickListener {
val dialog = val dialog =
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) 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() dialog.show()
// If user has clicked the switch do not interfere // If user has clicked the switch do not interfere
var preventAutoSwitching = false var preventAutoSwitching = false
dialog.hls_switch?.setOnClickListener { binding.hlsSwitch.setOnClickListener {
preventAutoSwitching = true preventAutoSwitching = true
} }
fun activateSwitchOnHls(text: String?) { fun activateSwitchOnHls(text: String?) {
dialog.hls_switch?.isChecked = normalSafeApiCall { binding.hlsSwitch.isChecked = normalSafeApiCall {
URI(text).path?.substringAfterLast(".")?.contains("m3u") URI(text).path?.substringAfterLast(".")?.contains("m3u")
} == true } == true
} }
dialog.stream_referer?.doOnTextChanged { text, _, _, _ -> binding.streamReferer.doOnTextChanged { text, _, _, _ ->
if (!preventAutoSwitching) if (!preventAutoSwitching)
activateSwitchOnHls(text?.toString()) activateSwitchOnHls(text?.toString())
} }
@ -210,16 +223,16 @@ class DownloadFragment : Fragment() {
0 0
)?.text?.toString()?.let { copy -> )?.text?.toString()?.let { copy ->
val fixedText = copy.trim() val fixedText = copy.trim()
dialog.stream_url?.setText(fixedText) binding.streamUrl.setText(fixedText)
activateSwitchOnHls(fixedText) activateSwitchOnHls(fixedText)
} }
dialog.apply_btt?.setOnClickListener { binding.applyBtt.setOnClickListener {
val url = dialog.stream_url.text?.toString() val url = binding.streamUrl.text?.toString()
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
showToast(activity, R.string.error_invalid_url, Toast.LENGTH_SHORT) showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT)
} else { } else {
val referer = dialog.stream_referer.text?.toString() val referer = binding.streamReferer.text?.toString()
activity?.navigate( activity?.navigate(
R.id.global_to_navigation_player, R.id.global_to_navigation_player,
@ -228,7 +241,7 @@ class DownloadFragment : Fragment() {
listOf(BasicLink(url)), listOf(BasicLink(url)),
extract = true, extract = true,
referer = referer, 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) dialog.dismissSafe(activity)
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
download_list?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val dy = scrollY - oldScrollY val dy = scrollY - oldScrollY
if (dy > 0) { //check for scroll down if (dy > 0) { //check for scroll down
download_stream_button?.shrink() // hide binding?.downloadStreamButton?.shrink() // hide
} else if (dy < -5) { } else if (dy < -5) {
download_stream_button?.extend() // show binding?.downloadStreamButton?.extend() // show
} }
} }
} }
downloadsViewModel.updateList(requireContext()) 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import androidx.core.view.isVisible
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.core.widget.ContentLoadingProgressBar
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.android.synthetic.main.download_header_episode.view.*
import java.util.* import java.util.*
data class VisualDownloadHeaderCached( data class VisualDownloadHeaderCached(
@ -26,7 +23,10 @@ data class VisualDownloadHeaderCached(
val child: VideoDownloadHelper.DownloadEpisodeCached?, 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( class DownloadHeaderAdapter(
var cardList: List<VisualDownloadHeaderCached>, var cardList: List<VisualDownloadHeaderCached>,
@ -34,39 +34,13 @@ class DownloadHeaderAdapter(
private val movieClickCallback: (DownloadClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return DownloadHeaderViewHolder( return DownloadHeaderViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.download_header_episode, parent, false), DownloadHeaderEpisodeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
),
clickCallback, clickCallback,
movieClickCallback movieClickCallback
) )
@ -76,7 +50,6 @@ class DownloadHeaderAdapter(
when (holder) { when (holder) {
is DownloadHeaderViewHolder -> { is DownloadHeaderViewHolder -> {
holder.bind(cardList[position]) holder.bind(cardList[position])
mBoundViewHolders.add(holder)
} }
} }
} }
@ -87,93 +60,89 @@ class DownloadHeaderAdapter(
class DownloadHeaderViewHolder class DownloadHeaderViewHolder
constructor( constructor(
itemView: View, val binding: DownloadHeaderEpisodeBinding,
private val clickCallback: (DownloadHeaderClickEvent) -> Unit, private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
private val movieClickCallback: (DownloadClickEvent) -> Unit, private val movieClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { ) : RecyclerView.ViewHolder(binding.root) {
override var downloadButton = EasyDownloadButton()
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 title: TextView = itemView.download_header_title
private val extraInfo: TextView = itemView.download_header_info private val extraInfo: TextView = itemView.download_header_info
private val holder: CardView = itemView.episode_holder private val holder: CardView = itemView.episode_holder
private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded
private val downloadImage: ImageView = itemView.download_header_episode_download private val downloadImage: ImageView = itemView.download_header_episode_download
private val normalImage: ImageView = itemView.download_header_goto_child private val normalImage: ImageView = itemView.download_header_goto_child*/
private var localCard: VisualDownloadHeaderCached? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(card: VisualDownloadHeaderCached) { fun bind(card: VisualDownloadHeaderCached) {
localCard = card
val d = card.data val d = card.data
poster?.setImage(d.poster) binding.downloadHeaderPoster.apply {
poster?.setOnClickListener { setImage(d.poster)
clickCallback.invoke(DownloadHeaderClickEvent(1, d)) setOnClickListener {
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
}
} }
title.text = d.name binding.apply {
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
//val isMovie = d.type.isMovieType() binding.downloadHeaderTitle.text = d.name
if (card.child != null) { val mbString = formatShortFileSize(itemView.context, card.totalBytes)
downloadBar.visibility = View.VISIBLE
downloadImage.visibility = View.VISIBLE
normalImage.visibility = View.GONE
/*setUpButton(
card.currentBytes,
card.totalBytes,
downloadBar,
downloadImage,
extraInfo,
card.child,
movieClickCallback
)*/
holder.setOnClickListener { //val isMovie = d.type.isMovieType()
movieClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child)) if (card.child != null) {
} //downloadHeaderProgressDownloaded.visibility = View.VISIBLE
} else {
downloadBar.visibility = View.GONE
downloadImage.visibility = View.GONE
normalImage.visibility = View.VISIBLE
try { // downloadHeaderEpisodeDownload.visibility = View.VISIBLE
extraInfo.text = binding.downloadHeaderGotoChild.visibility = View.GONE
extraInfo.context.getString(R.string.extra_info_format).format(
card.totalDownloads, downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback)
if (card.totalDownloads == 1) extraInfo.context.getString(R.string.episode) else extraInfo.context.getString( downloadButton.isVisible = true
R.string.episodes /*setUpButton(
), card.currentBytes,
mbString 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 } else {
extraInfo.text = "Error" downloadButton.isVisible = false
logError(t) // 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 package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse 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.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder 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.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx 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( class HomeChildItemAdapter(
val cardList: MutableList<SearchResponse>, val cardList: MutableList<SearchResponse>,
private val overrideLayout: Int? = null,
private val nextFocusUp: Int? = null, private val nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null, private val nextFocusDown: Int? = null,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
@ -26,16 +27,28 @@ class HomeChildItemAdapter(
var hasNext: Boolean = false var hasNext: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = overrideLayout val expanded = parent.context.IsBottomLayout()
?: if (parent.context.IsBottomLayout()) R.layout.home_result_grid_expanded else R.layout.home_result_grid /* 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( return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false), binding,
clickCallback, clickCallback,
itemCount, itemCount,
nextFocusUp, nextFocusUp,
nextFocusDown, nextFocusDown,
isHorizontal isHorizontal,
parent.isRtl()
) )
} }
@ -69,14 +82,15 @@ class HomeChildItemAdapter(
class CardViewHolder class CardViewHolder
constructor( constructor(
itemView: View, val binding: ViewBinding,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
var itemCount: Int, var itemCount: Int,
private val nextFocusUp: Int? = null, private val nextFocusUp: Int? = null,
private val nextFocusDown: 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) { fun bind(card: SearchResponse, position: Int) {
@ -87,26 +101,71 @@ class HomeChildItemAdapter(
else -> null else -> null
} }
(itemView.image_holder ?: itemView.background_card)?.apply { if (position == 0) { // to fix tv
val min = 114.toPx if (isRtl) {
val max = 180.toPx itemView.nextFocusRightId = R.id.nav_rail_view
itemView.nextFocusLeftId = -1
layoutParams = }
layoutParams.apply { else {
width = if (!isHorizontal) { itemView.nextFocusLeftId = R.id.nav_rail_view
min itemView.nextFocusRightId = -1
} else { }
max } else {
} itemView.nextFocusRightId = -1
height = if (!isHorizontal) { itemView.nextFocusLeftId = -1
max
} else {
min
}
}
} }
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( SearchResultBuilder.bind(
clickCallback, clickCallback,
card, card,
@ -118,9 +177,6 @@ class HomeChildItemAdapter(
) )
itemView.tag = position 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) //val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f)
//ani.fillAfter = true //ani.fillAfter = true
//ani.duration = 200 //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.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey 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.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent
import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent 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.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe 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.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi 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.WatchType
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment 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.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings 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.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API 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.* import java.util.*
@ -125,22 +114,26 @@ class HomeFragment : Fragment() {
expand: HomeViewModel.ExpandableHomepageList, expand: HomeViewModel.ExpandableHomepageList,
deleteCallback: (() -> Unit)? = null, deleteCallback: (() -> Unit)? = null,
expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null, expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null,
dismissCallback : (() -> Unit), dismissCallback: (() -> Unit),
): BottomSheetDialog { ): BottomSheetDialog {
val context = this val context = this
val bottomSheetDialogBuilder = BottomSheetDialog(context) val bottomSheetDialogBuilder = BottomSheetDialog(context)
val binding: HomeEpisodesExpandedBinding = HomeEpisodesExpandedBinding.inflate(
bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded) bottomSheetDialogBuilder.layoutInflater,
val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!! null,
false
)
bottomSheetDialogBuilder.setContentView(binding.root)
//val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
//title.findViewTreeLifecycleOwner().lifecycle.addObserver() //title.findViewTreeLifecycleOwner().lifecycle.addObserver()
val item = expand.list val item = expand.list
title.text = item.name binding.homeExpandedText.text = item.name
val recycle = // val recycle =
bottomSheetDialogBuilder.findViewById<AutofitRecyclerView>(R.id.home_expanded_recycler)!! // bottomSheetDialogBuilder.findViewById<AutofitRecyclerView>(R.id.home_expanded_recycler)!!
val titleHolder = //val titleHolder =
bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!! // bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!!
// main { // main {
//(bottomSheetDialogBuilder.ownerActivity as androidx.fragment.app.FragmentActivity?)?.supportFragmentManager?.fragments?.lastOrNull()?.viewLifecycleOwner?.apply { //(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 //val delete = bottomSheetDialogBuilder.home_expanded_delete
delete.isGone = deleteCallback == null binding.homeExpandedDelete.isGone = deleteCallback == null
if (deleteCallback != null) { if (deleteCallback != null) {
delete.setOnClickListener { binding.homeExpandedDelete.setOnClickListener {
try { try {
val builder: AlertDialog.Builder = AlertDialog.Builder(context) val builder: AlertDialog.Builder = AlertDialog.Builder(context)
val dialogClickListener = val dialogClickListener =
@ -172,6 +165,7 @@ class HomeFragment : Fragment() {
deleteCallback.invoke() deleteCallback.invoke()
bottomSheetDialogBuilder.dismissSafe(this) bottomSheetDialogBuilder.dismissSafe(this)
} }
DialogInterface.BUTTON_NEGATIVE -> {} DialogInterface.BUTTON_NEGATIVE -> {}
} }
} }
@ -191,26 +185,27 @@ class HomeFragment : Fragment() {
} }
} }
} }
binding.homeExpandedDragDown.setOnClickListener {
titleHolder.setOnClickListener {
bottomSheetDialogBuilder.dismissSafe(this) bottomSheetDialogBuilder.dismissSafe(this)
} }
// Span settings // Span settings
recycle.spanCount = currentSpan binding.homeExpandedRecycler.spanCount = currentSpan
recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback -> binding.homeExpandedRecycler.adapter =
handleSearchClickCallback(this, callback) SearchAdapter(item.list.toMutableList(), binding.homeExpandedRecycler) { callback ->
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) { handleSearchClickCallback(callback)
bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
//bottomSheetDialogBuilder.dismissSafe(this) 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 var expandCount = 0
val name = expand.list.name val name = expand.list.name
@ -238,7 +233,7 @@ class HomeFragment : Fragment() {
}) })
val spanListener = { span: Int -> val spanListener = { span: Int ->
recycle.spanCount = span binding.homeExpandedRecycler.spanCount = span
//(recycle.adapter as SearchAdapter).notifyDataSetChanged() //(recycle.adapter as SearchAdapter).notifyDataSetChanged()
} }
@ -280,19 +275,19 @@ class HomeFragment : Fragment() {
) )
} }
private fun getPairList(header: ChipGroup) = getPairList( private fun getPairList(header: TvtypesChipsBinding) = getPairList(
header.home_select_anime, header.homeSelectAnime,
header.home_select_cartoons, header.homeSelectCartoons,
header.home_select_tv_series, header.homeSelectTvSeries,
header.home_select_documentaries, header.homeSelectDocumentaries,
header.home_select_movies, header.homeSelectMovies,
header.home_select_asian, header.homeSelectAsian,
header.home_select_livestreams, header.homeSelectLivestreams,
header.home_select_nsfw, header.homeSelectNsfw,
header.home_select_others header.homeSelectOthers
) )
fun validateChips(header: ChipGroup?, validTypes: List<TvType>) { fun validateChips(header: TvtypesChipsBinding?, validTypes: List<TvType>) {
if (header == null) return if (header == null) return
val pairList = getPairList(header) val pairList = getPairList(header)
for ((button, types) in pairList) { 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 if (header == null) return
val pairList = getPairList(header) val pairList = getPairList(header)
for ((button, types) in pairList) { for ((button, types) in pairList) {
@ -311,7 +306,7 @@ class HomeFragment : Fragment() {
} }
fun bindChips( fun bindChips(
header: ChipGroup?, header: TvtypesChipsBinding?,
selectedTypes: List<TvType>, selectedTypes: List<TvType>,
validTypes: List<TvType>, validTypes: List<TvType>,
callback: (List<TvType>) -> Unit callback: (List<TvType>) -> Unit
@ -344,7 +339,13 @@ class HomeFragment : Fragment() {
BottomSheetDialog(this) BottomSheetDialog(this)
builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED 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.show()
builder.let { dialog -> builder.let { dialog ->
val isMultiLang = getApiProviderLangSettings().let { set -> val isMultiLang = getApiProviderLangSettings().let { set ->
@ -360,14 +361,11 @@ class HomeFragment : Fragment() {
?.toMutableList() ?.toMutableList()
?: mutableListOf(TvType.Movie, TvType.TvSeries) ?: mutableListOf(TvType.Movie, TvType.TvSeries)
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt) binding.cancelBtt.setOnClickListener {
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
cancelBtt?.setOnClickListener {
dialog.dismissSafe() dialog.dismissSafe()
} }
applyBtt?.setOnClickListener { binding.applyBtt.setOnClickListener {
if (currentApiName != selectedApiName) { if (currentApiName != selectedApiName) {
currentApiName?.let(callback) currentApiName?.let(callback)
} }
@ -408,7 +406,7 @@ class HomeFragment : Fragment() {
} }
bindChips( bindChips(
dialog.home_select_group, binding.tvtypesChipsScroll.tvtypesChips,
preSelectedTypes, preSelectedTypes,
validAPIs.flatMap { it.supportedTypes }.distinct() validAPIs.flatMap { it.supportedTypes }.distinct()
) { list -> ) { list ->
@ -423,6 +421,9 @@ class HomeFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels()
var binding: FragmentHomeBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -430,14 +431,25 @@ class HomeFragment : Fragment() {
): View? { ): View? {
//homeViewModel = //homeViewModel =
// ViewModelProvider(this).get(HomeViewModel::class.java) // ViewModelProvider(this).get(HomeViewModel::class.java)
bottomSheetDialog?.ownShow() bottomSheetDialog?.ownShow()
val layout = val layout =
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home 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() { override fun onDestroyView() {
bottomSheetDialog?.ownHide() bottomSheetDialog?.ownHide()
binding = null
super.onDestroyView() super.onDestroyView()
} }
@ -450,7 +462,7 @@ class HomeFragment : Fragment() {
private val apiChangeClickListener = View.OnClickListener { view -> private val apiChangeClickListener = View.OnClickListener { view ->
view.context.selectHomepage(currentApiName) { api -> view.context.selectHomepage(currentApiName) { api ->
homeViewModel.loadAndCancel(api) homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true)
} }
/*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf() /*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf()
@ -467,196 +479,149 @@ class HomeFragment : Fragment() {
fixGrid() 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 currentApiName: String? = null
private var toggleRandomButton = false private var toggleRandomButton = false
private var bottomSheetDialog: BottomSheetDialog? = null private var bottomSheetDialog: BottomSheetDialog? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fixGrid() fixGrid()
home_change_api_loading?.setOnClickListener(apiChangeClickListener) binding?.apply {
home_api_fab?.setOnClickListener(apiChangeClickListener) homeChangeApiLoading.setOnClickListener(apiChangeClickListener)
home_random?.setOnClickListener { //homeChangeApiLoading.setOnClickListener(apiChangeClickListener)
if (listHomepageItems.isNotEmpty()) { homeApiFab.setOnClickListener(apiChangeClickListener)
activity.loadSearchResult(listHomepageItems.random()) 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 //Load value for toggling Random button. Hide at startup
context?.let { context?.let {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it) val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
toggleRandomButton = toggleRandomButton =
settingsManager.getBoolean(getString(R.string.random_button_key), false) settingsManager.getBoolean(
home_random?.visibility = View.GONE getString(R.string.random_button_key),
} false
) && !isTvSettings()
observe(homeViewModel.preview) { preview -> binding?.homeRandom?.visibility = View.GONE
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData(
preview
)
} }
observe(homeViewModel.apiName) { apiName -> observe(homeViewModel.apiName) { apiName ->
currentApiName = apiName currentApiName = apiName
home_api_fab?.text = apiName binding?.homeApiFab?.text = apiName
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName(
apiName
)
} }
observe(homeViewModel.page) { data -> observe(homeViewModel.page) { data ->
when (data) { binding?.apply {
is Resource.Success -> { when (data) {
home_loading_shimmer?.stopShimmer() is Resource.Success -> {
homeLoadingShimmer.stopShimmer()
val d = data.value val d = data.value
val mutableListOfResponse = mutableListOf<SearchResponse>() val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear() listHomepageItems.clear()
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList( (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(
d.values.toMutableList(), d.values.toMutableList(),
home_master_recycler homeMasterRecycler
) )
home_loading?.isVisible = false homeLoading.isVisible = false
home_loading_error?.isVisible = false homeLoadingError.isVisible = false
home_master_recycler?.isVisible = true homeMasterRecycler.isVisible = true
//home_loaded?.isVisible = true //home_loaded?.isVisible = true
if (toggleRandomButton) { if (toggleRandomButton) {
//Flatten list //Flatten list
d.values.forEach { dlist -> d.values.forEach { dlist ->
mutableListOfResponse.addAll(dlist.list.list) 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) view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api ->
Pair(
home_reload_connection_open_in_browser.setOnClickListener { view -> index,
val validAPIs = apis//.filter { api -> api.hasMainPage } api.name
)
view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api -> }) {
Pair( try {
index, val i = Intent(Intent.ACTION_VIEW)
api.name i.data = Uri.parse(validAPIs[itemId].mainUrl)
) startActivity(i)
}) { } catch (e: Exception) {
try { logError(e)
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 is Resource.Loading -> {
home_loading_error?.isVisible = true (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf())
home_master_recycler?.isVisible = false homeLoadingShimmer.startShimmer()
//home_loaded?.isVisible = false homeLoading.isVisible = true
} homeLoadingError.isVisible = false
is Resource.Loading -> { homeMasterRecycler.isVisible = false
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf()) //home_loaded?.isVisible = false
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 })
} }
} }
} }
@ -665,72 +630,35 @@ class HomeFragment : Fragment() {
//context?.fixPaddingStatusbarView(home_statusbar) //context?.fixPaddingStatusbarView(home_statusbar)
//context?.fixPaddingStatusbar(home_padding) //context?.fixPaddingStatusbar(home_padding)
context?.fixPaddingStatusbar(home_loading_statusbar)
home_master_recycler?.adapter = observeNullable(homeViewModel.popup) { item ->
HomeParentItemAdapterPreview(mutableListOf(), { callback -> if (item == null) {
homeHandleSearch(callback) bottomSheetDialog?.dismissSafe()
}, { item -> bottomSheetDialog = null
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = { return@observeNullable
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)
} }
})
// 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 // nice profile pic on homepage
//home_profile_picture_holder?.isVisible = false //home_profile_picture_holder?.isVisible = false
// just in case // 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 //TODO READD THIS
/*for (syncApi in OAuth2Apis) { /*for (syncApi in OAuth2Apis) {
val login = syncApi.loginInfo() val login = syncApi.loginInfo()

View file

@ -3,50 +3,19 @@ package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView 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.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView 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.HomePageList
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.HomepageParentBinding
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.ui.result.setLinearListLayout 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.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable 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( class LoadClickCallback(
val action: Int = 0, val action: Int = 0,
@ -57,17 +26,23 @@ class LoadClickCallback(
open class ParentItemAdapter( open class ParentItemAdapter(
private var items: MutableList<HomeViewModel.ExpandableHomepageList>, private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
//private val viewModel: HomeViewModel,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null, private val expandCallback: ((String) -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 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( return ParentViewHolder(
LayoutInflater.from(parent.context).inflate( binding,
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
parent,
false
),
clickCallback, clickCallback,
moreInfoClickCallback, moreInfoClickCallback,
expandCallback expandCallback
@ -178,14 +153,15 @@ open class ParentItemAdapter(
class ParentViewHolder class ParentViewHolder
constructor( constructor(
itemView: View, val binding: HomepageParentBinding,
// val viewModel: HomeViewModel,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null, private val expandCallback: ((String) -> Unit)? = null,
) : ) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
val title: TextView = itemView.home_child_more_info val title: TextView = binding.homeChildMoreInfo
private val recyclerView: RecyclerView = itemView.home_child_recyclerview private val recyclerView: RecyclerView = binding.homeChildRecyclerview
fun update(expand: HomeViewModel.ExpandableHomepageList) { fun update(expand: HomeViewModel.ExpandableHomepageList) {
val info = expand.list val info = expand.list

View file

@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.home
import android.content.res.Configuration import android.content.res.Configuration
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.LoadResponse 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 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 : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class HomeScrollAdapter(
@LayoutRes val layout: Int = R.layout.home_scroll_view,
private val forceHorizontalPosters: Boolean? = null
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: MutableList<LoadResponse> = mutableListOf() private var items: MutableList<LoadResponse> = mutableListOf()
var hasMoreItems: Boolean = false var hasMoreItems: Boolean = false
@ -45,9 +39,16 @@ class HomeScrollAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 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( return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false), binding,
forceHorizontalPosters //forceHorizontalPosters
) )
} }
@ -61,22 +62,32 @@ class HomeScrollAdapter(
class CardViewHolder class CardViewHolder
constructor( constructor(
itemView: View, val binding: ViewBinding,
private val forceHorizontalPosters: Boolean? = null //private val forceHorizontalPosters: Boolean? = null
) : ) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
fun bind(card: LoadResponse) { fun bind(card: LoadResponse) {
card.apply { val isHorizontal =
val isHorizontal = binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
(forceHorizontalPosters == true) || ((forceHorizontalPosters != false) && itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
val posterUrl = if (isHorizontal) backgroundPosterUrl ?: posterUrl else posterUrl val posterUrl =
?: backgroundPosterUrl if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
itemView.home_scroll_preview_tags?.text = tags?.joinToString("") ?: "" ?: card.backgroundPosterUrl
itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty()
itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders) when (binding) {
itemView.home_scroll_preview_title?.text = name 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 package com.lagradost.cloudstream3.ui.home
import android.os.Build
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia 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.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey 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
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.WatchType 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.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
@ -32,7 +51,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.EnumSet
import kotlin.collections.set import kotlin.collections.set
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
@ -72,7 +91,7 @@ class HomeViewModel : ViewModel() {
} }
} }
private var repo: APIRepository? = null var repo: APIRepository? = null
private val _apiName = MutableLiveData<String>() private val _apiName = MutableLiveData<String>()
val apiName: LiveData<String> = _apiName val apiName: LiveData<String> = _apiName
@ -83,7 +102,7 @@ class HomeViewModel : ViewModel() {
private var currentShuffledList: List<SearchResponse> = listOf() private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository { private fun autoloadRepo(): APIRepository {
return APIRepository(apis.first { it.hasMainPage }) return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }})
} }
private val _availableWatchStatusTypes = private val _availableWatchStatusTypes =
@ -101,8 +120,14 @@ class HomeViewModel : ViewModel() {
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
fun loadResumeWatching() = viewModelScope.launchSafe { private fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatchingResult = getResumeWatching() 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 { resumeWatchingResult?.let {
_resumeWatching.postValue(it) _resumeWatching.postValue(it)
} }
@ -128,6 +153,10 @@ class HomeViewModel : ViewModel() {
currentWatchTypes.remove(WatchType.NONE) currentWatchTypes.remove(WatchType.NONE)
if (currentWatchTypes.size <= 0) { if (currentWatchTypes.size <= 0) {
setKey(
HOME_BOOKMARK_VALUE_LIST,
intArrayOf()
)
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf()) _availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
_bookmarks.postValue(Pair(false, ArrayList())) _bookmarks.postValue(Pair(false, ArrayList()))
return@launchSafe return@launchSafe
@ -135,7 +164,10 @@ class HomeViewModel : ViewModel() {
val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first())
//if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(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( _availableWatchStatusTypes.postValue(
Pair( Pair(
watchPrefNotNull, watchPrefNotNull,
@ -152,8 +184,10 @@ class HomeViewModel : ViewModel() {
} }
private var onGoingLoad: Job? = null private var onGoingLoad: Job? = null
private fun loadAndCancel(api: MainAPI?) { private var isCurrentlyLoadingName : String? = null
private fun loadAndCancel(api: MainAPI) {
onGoingLoad?.cancel() onGoingLoad?.cancel()
isCurrentlyLoadingName = api.name
onGoingLoad = load(api) onGoingLoad = load(api)
} }
@ -255,12 +289,12 @@ class HomeViewModel : ViewModel() {
} }
} }
private fun load(api: MainAPI?) = ioSafe { private fun load(api: MainAPI) : Job = ioSafe {
repo = if (api != null) { repo = //if (api != null) {
APIRepository(api) APIRepository(api)
} else { //} else {
autoloadRepo() // autoloadRepo()
} //}
_apiName.postValue(repo?.name) _apiName.postValue(repo?.name)
_randomItems.postValue(listOf()) _randomItems.postValue(listOf())
@ -274,6 +308,7 @@ class HomeViewModel : ViewModel() {
_page.postValue(Resource.Loading()) _page.postValue(Resource.Loading())
_preview.postValue(Resource.Loading()) _preview.postValue(Resource.Loading())
// cancel the current preview expand as that is no longer relevant
addJob?.cancel() addJob?.cancel()
when (val data = repo?.getMainPage(1, null)) { when (val data = repo?.getMainPage(1, null)) {
@ -337,41 +372,126 @@ class HomeViewModel : ViewModel() {
logError(e) logError(e)
} }
} }
is Resource.Failure -> { is Resource.Failure -> {
_page.postValue(data!!) _page.postValue(data!!)
_preview.postValue(data!!) _preview.postValue(data!!)
} }
else -> Unit 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. // 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 // The issue with this is that the homepage may be fetched multiple times while the first request is loading
val api = getApiFromNameNull(preferredApiName) val api = getApiFromNameNull(preferredApiName)
if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { // api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true
return@launchSafe 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) { 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) loadAndCancel(noneApi)
} else if (preferredApiName == randomApi.name) { } 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() val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) { if (validAPIs.isNullOrEmpty()) {
// Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded
loadAndCancel(noneApi) loadAndCancel(noneApi)
} else { } else {
val apiRandom = validAPIs.random() val apiRandom = validAPIs.random()
loadAndCancel(apiRandom) 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) { } 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 { } 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) loadAndCancel(api)
} }
} }

View file

@ -6,7 +6,6 @@ import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -14,6 +13,7 @@ import android.view.animation.AlphaAnimation
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.lagradost.cloudstream3.APIHolder 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.openBrowser
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.observe 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.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import kotlinx.android.synthetic.main.fragment_library.*
import kotlin.math.abs import kotlin.math.abs
const val LIBRARY_FOLDER = "library_folder" const val LIBRARY_FOLDER = "library_folder"
@ -73,14 +73,25 @@ class LibraryFragment : Fragment() {
private val libraryViewModel: LibraryViewModel by activityViewModels() private val libraryViewModel: LibraryViewModel by activityViewModels()
var binding: FragmentLibraryBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.fragment_library, container, false) 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) { override fun onSaveInstanceState(outState: Bundle) {
viewpager?.currentItem?.let { currentItem -> binding?.viewpager?.currentItem?.let { currentItem ->
outState.putInt(VIEWPAGER_ITEM_KEY, currentItem) outState.putInt(VIEWPAGER_ITEM_KEY, currentItem)
} }
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
@ -88,9 +99,9 @@ class LibraryFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(search_status_bar_padding) fixPaddingStatusbar(binding?.searchStatusBarPadding)
sort_fab?.setOnClickListener { binding?.sortFab?.setOnClickListener {
val methods = libraryViewModel.sortingMethods.map { val methods = libraryViewModel.sortingMethods.map {
txt(it.stringRes).asString(view.context) 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 { override fun onQueryTextSubmit(query: String?): Boolean {
libraryViewModel.sort(ListSorting.Query, query) libraryViewModel.sort(ListSorting.Query, query)
return true return true
@ -129,7 +140,7 @@ class LibraryFragment : Fragment() {
libraryViewModel.reloadPages(false) libraryViewModel.reloadPages(false)
list_selector?.setOnClickListener { binding?.listSelector?.setOnClickListener {
val items = libraryViewModel.availableApiNames val items = libraryViewModel.availableApiNames
val currentItem = libraryViewModel.currentApiName.value val currentItem = libraryViewModel.currentApiName.value
@ -152,12 +163,14 @@ class LibraryFragment : Fragment() {
syncId: SyncIdName, syncId: SyncIdName,
apiName: String? = null, apiName: String? = null,
) { ) {
val availableProviders = allProviders.filter { val availableProviders = synchronized(allProviders) {
it.supportedSyncNames.contains(syncId) allProviders.filter {
}.map { it.name } + it.supportedSyncNames.contains(syncId)
// Add the api if it exists }.map { it.name } +
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList()) // Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList())
}
val baseOptions = listOf( val baseOptions = listOf(
LibraryOpenerType.Default, LibraryOpenerType.Default,
LibraryOpenerType.None, LibraryOpenerType.None,
@ -209,20 +222,22 @@ class LibraryFragment : Fragment() {
} }
} }
provider_selector?.setOnClickListener { binding?.providerSelector?.setOnClickListener {
val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener
activity?.showPluginSelectionDialog(syncName.name, syncName) activity?.showPluginSelectionDialog(syncName.name, syncName)
} }
viewpager?.setPageTransformer(LibraryScrollTransformer()) binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
viewpager?.adapter = binding?.viewpager?.adapter =
viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean -> binding?.viewpager?.adapter ?: ViewpagerAdapter(
if (isScrollingDown) { mutableListOf(),
sort_fab?.shrink() { isScrollingDown: Boolean ->
} else { if (isScrollingDown) {
sort_fab?.extend() binding?.sortFab?.shrink()
} } else {
}) callback@{ searchClickCallback -> binding?.sortFab?.extend()
}
}) callback@{ searchClickCallback ->
// To prevent future accidents // To prevent future accidents
debugAssert({ debugAssert({
searchClickCallback.card !is SyncAPI.LibraryItem searchClickCallback.card !is SyncAPI.LibraryItem
@ -267,6 +282,7 @@ class LibraryFragment : Fragment() {
) )
} }
} }
LibraryOpenerType.None -> {} LibraryOpenerType.None -> {}
LibraryOpenerType.Provider -> LibraryOpenerType.Provider ->
savedSelection.providerData?.apiName?.let { apiName -> savedSelection.providerData?.apiName?.let { apiName ->
@ -275,8 +291,10 @@ class LibraryFragment : Fragment() {
apiName, apiName,
) )
} }
LibraryOpenerType.Browser -> LibraryOpenerType.Browser ->
openBrowser(searchClickCallback.card.url) openBrowser(searchClickCallback.card.url)
LibraryOpenerType.Search -> { LibraryOpenerType.Search -> {
QuickSearchFragment.pushSearch( QuickSearchFragment.pushSearch(
activity, activity,
@ -288,22 +306,28 @@ class LibraryFragment : Fragment() {
} }
} }
viewpager?.offscreenPageLimit = 2 binding?.apply {
viewpager?.reduceDragSensitivity() viewpager.offscreenPageLimit = 2
viewpager.reduceDragSensitivity()
}
val startLoading = Runnable { val startLoading = Runnable {
gridview?.numColumns = context?.getSpanCount() ?: 3 binding?.apply {
gridview?.adapter = gridview.numColumns = context?.getSpanCount() ?: 3
context?.let { LoadingPosterAdapter(it, 6 * 3) } gridview.adapter =
library_loading_overlay?.isVisible = true context?.let { LoadingPosterAdapter(it, 6 * 3) }
library_loading_shimmer?.startShimmer() libraryLoadingOverlay.isVisible = true
empty_list_textview?.isVisible = false libraryLoadingShimmer.startShimmer()
emptyListTextview.isVisible = false
}
} }
val stopLoading = Runnable { val stopLoading = Runnable {
gridview?.adapter = null binding?.apply {
library_loading_overlay?.isVisible = false gridview.adapter = null
library_loading_shimmer?.stopShimmer() libraryLoadingOverlay.isVisible = false
libraryLoadingShimmer.stopShimmer()
}
} }
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
@ -314,65 +338,75 @@ class LibraryFragment : Fragment() {
handler.removeCallbacks(startLoading) handler.removeCallbacks(startLoading)
val pages = resource.value val pages = resource.value
val showNotice = pages.all { it.items.isEmpty() } val showNotice = pages.all { it.items.isEmpty() }
empty_list_textview?.isVisible = showNotice
if (showNotice) {
if (libraryViewModel.availableApiNames.size > 1) { binding?.apply {
empty_list_textview?.setText(R.string.empty_library_logged_in_message) emptyListTextview.isVisible = showNotice
} else { if (showNotice) {
empty_list_textview?.setText(R.string.empty_library_no_accounts_message) 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 -> { is Resource.Loading -> {
// Only start loading after 200ms to prevent loading cached lists // Only start loading after 200ms to prevent loading cached lists
handler.postDelayed(startLoading, 200) handler.postDelayed(startLoading, 200)
} }
is Resource.Failure -> { is Resource.Failure -> {
stopLoading.run() stopLoading.run()
// No user indication it failed :( // No user indication it failed :(
@ -383,7 +417,7 @@ class LibraryFragment : Fragment() {
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
(viewpager.adapter as? ViewpagerAdapter)?.rebind() (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }
} }

View file

@ -2,13 +2,13 @@ package com.lagradost.cloudstream3.ui.library
import android.view.View import android.view.View
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import kotlinx.android.synthetic.main.library_viewpager_page.view.* import com.lagradost.cloudstream3.R
import kotlin.math.roundToInt import kotlin.math.roundToInt
class LibraryScrollTransformer : ViewPager2.PageTransformer { class LibraryScrollTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) { override fun transformPage(page: View, position: Float) {
val padding = (-position * page.width).roundToInt() val padding = (-position * page.width).roundToInt()
page.page_recyclerview.setPadding( page.findViewById<View>(R.id.page_recyclerview).setPadding(
padding, 0, padding, 0,
-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.AccountManager.Companion.SyncApis
import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncAPI
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import kotlinx.coroutines.delay
enum class ListSorting(@StringRes val stringRes: Int) { enum class ListSorting(@StringRes val stringRes: Int) {
Query(R.string.none), Query(R.string.none),

View file

@ -5,15 +5,7 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.BaseAdapter 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.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) : class LoadingPosterAdapter(context: Context, private val itemCount: Int) :
BaseAdapter() { BaseAdapter() {

View file

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

View file

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

View file

@ -11,6 +11,9 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.Toast import android.widget.Toast
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.annotation.StringRes 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
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage 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) { enum class PlayerResize(@StringRes val nameRes: Int) {
Fit(R.string.resize_fit), Fit(R.string.resize_fit),
@ -71,9 +72,15 @@ abstract class AbstractPlayerFragment(
var isBuffering = true var isBuffering = true
protected open var hasPipModeSupport = 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 @LayoutRes
protected var layout: Int = R.layout.fragment_player protected open var layout: Int = R.layout.fragment_player
open fun nextEpisode() { open fun nextEpisode() {
throw NotImplementedError() throw NotImplementedError()
@ -132,15 +139,15 @@ abstract class AbstractPlayerFragment(
isBuffering = CSPlayerLoading.IsBuffering == isPlaying isBuffering = CSPlayerLoading.IsBuffering == isPlaying
if (isBuffering) { if (isBuffering) {
player_pause_play_holder_holder?.isVisible = false playerPausePlayHolderHolder?.isVisible = false
player_buffering?.isVisible = true playerBuffering?.isVisible = true
} else { } else {
player_pause_play_holder_holder?.isVisible = true playerPausePlayHolderHolder?.isVisible = true
player_buffering?.isVisible = false playerBuffering?.isVisible = false
if (wasPlaying != isPlaying) { if (wasPlaying != isPlaying) {
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play) playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
val drawable = player_pause_play?.drawable val drawable = playerPausePlay?.drawable
var startedAnimation = false var startedAnimation = false
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
@ -162,10 +169,10 @@ abstract class AbstractPlayerFragment(
// somehow the phone is wacked // somehow the phone is wacked
if (!startedAnimation) { 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 { } 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) { fun showToast(message: String, gotoNext: Boolean = false) {
if (gotoNext && hasNextMirror()) { if (gotoNext && hasNextMirror()) {
showToast( showToast(
activity,
message, message,
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
) )
nextMirror() nextMirror()
} else { } else {
showToast( showToast(
activity,
context?.getString(R.string.no_links_found_toast) + "\n" + message, context?.getString(R.string.no_links_found_toast) + "\n" + message,
Toast.LENGTH_LONG Toast.LENGTH_LONG
) )
@ -327,9 +332,9 @@ abstract class AbstractPlayerFragment(
} }
// Necessary for multiple combined videos // Necessary for multiple combined videos
player_view?.setShowMultiWindowTimeBar(true) playerView?.setShowMultiWindowTimeBar(true)
player_view?.player = player playerView?.player = player
player_view?.performClick() playerView?.performClick()
} }
} }
@ -386,9 +391,9 @@ abstract class AbstractPlayerFragment(
) )
if (player is CS3IPlayer) { if (player is CS3IPlayer) {
subView = player_view?.findViewById(R.id.exo_subtitles) subView = playerView?.findViewById(R.id.exo_subtitles)
subStyle = SubtitlesFragment.getCurrentSavedStyle() subStyle = SubtitlesFragment.getCurrentSavedStyle()
player.initSubtitles(subView, subtitle_holder, subStyle) player.initSubtitles(subView, subtitleHolder, subStyle)
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
@ -457,10 +462,10 @@ abstract class AbstractPlayerFragment(
PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
PlayerResize.Zoom -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM PlayerResize.Zoom -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
} }
player_view?.resizeMode = type playerView?.resizeMode = type
if (showToast) if (showToast)
showToast(activity, resize.nameRes, Toast.LENGTH_SHORT) showToast(resize.nameRes, Toast.LENGTH_SHORT)
} }
override fun onStop() { override fun onStop() {
@ -481,6 +486,13 @@ abstract class AbstractPlayerFragment(
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): 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 TAG = "CS3ExoPlayer"
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language" 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 { class CS3IPlayer : IPlayer {
private var isPlaying = false private var isPlaying = false
@ -721,7 +729,7 @@ class CS3IPlayer : IPlayer {
) )
) )
// Allows any seeking to be +- 0.3s to allow for faster seeking // Allows any seeking to be +- 0.3s to allow for faster seeking
.setSeekParameters(SeekParameters(300_000, 300_000)) .setSeekParameters(SeekParameters(toleranceBeforeUs, toleranceAfterUs))
.setLoadControl( .setLoadControl(
DefaultLoadControl.Builder() DefaultLoadControl.Builder()
.setTargetBufferBytes( .setTargetBufferBytes(
@ -788,7 +796,7 @@ class CS3IPlayer : IPlayer {
private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? { private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? {
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
for (lastTimeStamp in lastTimeStamps) { for (lastTimeStamp in lastTimeStamps) {
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) { if (lastTimeStamp.startMs <= position && (position + (toleranceBeforeUs / 1000L) + 1) < lastTimeStamp.endMs) {
return lastTimeStamp return lastTimeStamp
} }
} }
@ -796,11 +804,12 @@ class CS3IPlayer : IPlayer {
} }
fun updatedTime(writePosition: Long? = null) { fun updatedTime(writePosition: Long? = null) {
getCurrentTimestamp(writePosition)?.let { timestamp -> val position = writePosition ?: exoPlayer?.currentPosition
getCurrentTimestamp(position)?.let { timestamp ->
onTimestampInvoked?.invoke(timestamp) onTimestampInvoked?.invoke(timestamp)
} }
val position = writePosition ?: exoPlayer?.currentPosition
val duration = exoPlayer?.contentDuration val duration = exoPlayer?.contentDuration
if (duration != null && position != null) { if (duration != null && position != null) {
playerPositionChanged?.invoke(Pair(position, duration)) playerPositionChanged?.invoke(Pair(position, duration))
@ -1086,9 +1095,9 @@ class CS3IPlayer : IPlayer {
} }
override fun onRenderedFirstFrame() { override fun onRenderedFirstFrame() {
updatedTime()
super.onRenderedFirstFrame() super.onRenderedFirstFrame()
onRenderFirst() onRenderFirst()
updatedTime()
} }
}) })
} catch (e: Exception) { } catch (e: Exception) {
@ -1116,42 +1125,43 @@ class CS3IPlayer : IPlayer {
} }
fun onRenderFirst() { fun onRenderFirst() {
if (!hasUsedFirstRender) { // this insures that we only call this once per player load if (hasUsedFirstRender) { // this insures that we only call this once per player load
Log.i(TAG, "Rendered first frame") return
val invalid = exoPlayer?.duration?.let { duration -> }
// Only errors short playback when not playing downloaded files Log.i(TAG, "Rendered first frame")
duration < 20_000L && currentDownloadedFile == null hasUsedFirstRender = true
// Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period val invalid = exoPlayer?.duration?.let { duration ->
// If you can get the total time that'd be better, but this is already niche. // Only errors short playback when not playing downloaded files
&& exoPlayer?.currentTimeline?.periodCount == 1 duration < 20_000L && currentDownloadedFile == null
&& exoPlayer?.isCurrentMediaItemLive != true // Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period
} ?: false // 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) { if (invalid) {
releasePlayer(saveTime = false) releasePlayer(saveTime = false)
playerError?.invoke(InvalidFileException("Too short playback")) playerError?.invoke(InvalidFileException("Too short playback"))
return return
} }
setPreferredSubtitles(currentSubtitles) setPreferredSubtitles(currentSubtitles)
hasUsedFirstRender = true val format = exoPlayer?.videoFormat
val format = exoPlayer?.videoFormat val width = format?.width
val width = format?.width val height = format?.height
val height = format?.height if (height != null && width != null) {
if (height != null && width != null) { playerDimensionsLoaded?.invoke(Pair(width, height))
playerDimensionsLoaded?.invoke(Pair(width, height)) updatedTime()
updatedTime() exoPlayer?.apply {
exoPlayer?.apply { requestedListeningPercentages?.forEach { percentage ->
requestedListeningPercentages?.forEach { percentage -> createMessage { _, _ ->
createMessage { _, _ -> updatedTime()
updatedTime()
}
.setLooper(Looper.getMainLooper())
.setPosition( /* positionMs= */contentDuration * percentage / 100)
// .setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send()
} }
.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.*
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.CommonActivity.showToast 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.mvvm.*
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders 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.PlayerSubtitleHelper.Companion.toSubtitleMimeType
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper 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.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.result.*
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 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.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.toPx 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 kotlinx.coroutines.Job
import java.util.* import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.math.abs import kotlin.math.abs
class GeneratorPlayer : FullScreenPlayer() { class GeneratorPlayer : FullScreenPlayer() {
@ -98,12 +91,14 @@ class GeneratorPlayer : FullScreenPlayer() {
private var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none private var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none
private var binding: FragmentPlayerBinding? = null
private fun startLoading() { private fun startLoading() {
player.release() player.release()
currentSelectedSubtitles = null currentSelectedSubtitles = null
isActive = false isActive = false
overlay_loading_skip_button?.isVisible = false binding?.overlayLoadingSkipButton?.isVisible = false
player_loading_overlay?.isVisible = true binding?.playerLoadingOverlay?.isVisible = true
} }
private fun setSubtitles(sub: SubtitleData?): Boolean { private fun setSubtitles(sub: SubtitleData?): Boolean {
@ -118,7 +113,7 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onTracksInfoChanged() { override fun onTracksInfoChanged() {
val tracks = player.getVideoTracks() val tracks = player.getVideoTracks()
player_tracks_btt?.isVisible = playerBinding?.playerTracksBtt?.isVisible =
tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1 tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
// Only set the preferred language if it is available. // Only set the preferred language if it is available.
// Otherwise it may give some users audio track init failed! // Otherwise it may give some users audio track init failed!
@ -158,12 +153,12 @@ class GeneratorPlayer : FullScreenPlayer() {
if (link == null) return if (link == null) return
// manage UI // manage UI
player_loading_overlay?.isVisible = false binding?.playerLoadingOverlay?.isVisible = false
uiReset() uiReset()
currentSelectedLink = link currentSelectedLink = link
currentMeta = viewModel.getMeta() currentMeta = viewModel.getMeta()
nextMeta = viewModel.getNextMeta() nextMeta = viewModel.getNextMeta()
setEpisodes(viewModel.getAllMeta() ?: emptyList()) // setEpisodes(viewModel.getAllMeta() ?: emptyList())
isActive = true isActive = true
setPlayerDimen(null) setPlayerDimen(null)
setTitle() setTitle()
@ -209,7 +204,7 @@ class GeneratorPlayer : FullScreenPlayer() {
closestQuality(linkData?.quality) closestQuality(linkData?.quality)
) )
val sourcePriority = val sourcePriority =
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name) QualityDataHelper.getSourcePriority(qualityProfile, linkData?.source)
// negative because we want to sort highest quality first // negative because we want to sort highest quality first
return qualityPriority + sourcePriority return qualityPriority + sourcePriority
@ -257,7 +252,9 @@ class GeneratorPlayer : FullScreenPlayer() {
val isSingleProvider = subsProviders.size == 1 val isSingleProvider = subsProviders.size == 1
val dialog = Dialog(context, R.style.AlertDialogCustomBlack) 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 currentSubtitles: List<AbstractSubtitleEntities.SubtitleEntity> = emptyList()
var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null
@ -295,6 +292,7 @@ class GeneratorPlayer : FullScreenPlayer() {
imageViewEnd.setImageDrawable(drawableEnd) imageViewEnd.setImageDrawable(drawableEnd)
} }
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: LayoutInflater.from(context).inflate(layout, null) val view = convertView ?: LayoutInflater.from(context).inflate(layout, null)
@ -318,16 +316,16 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
dialog.show() dialog.show()
dialog.cancel_btt.setOnClickListener { binding.cancelBtt.setOnClickListener {
dialog.dismissSafe() dialog.dismissSafe()
} }
dialog.subtitle_adapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
dialog.subtitle_adapter.adapter = arrayAdapter binding.subtitleAdapter.adapter = arrayAdapter
val adapter = 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 currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
} }
@ -343,16 +341,16 @@ class GeneratorPlayer : FullScreenPlayer() {
val currentTempMeta = getMetaData() val currentTempMeta = getMetaData()
// bruh idk why it is not correct // bruh idk why it is not correct
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent)) val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
dialog.search_loading_bar.progressTintList = color binding.searchLoadingBar.progressTintList = color
dialog.search_loading_bar.indeterminateTintList = color binding.searchLoadingBar.indeterminateTintList = color
observeNullable(viewModel.currentSubtitleYear) { observeNullable(viewModel.currentSubtitleYear) {
// When year is changed search again // When year is changed search again
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true) binding.subtitlesSearch.setQuery(binding.subtitlesSearch.query, true)
dialog.year_btt.text = it?.toString() ?: txt(R.string.none).asString(context) 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 none = txt(R.string.none).asString(context)
val currentYear = Calendar.getInstance().get(Calendar.YEAR) val currentYear = Calendar.getInstance().get(Calendar.YEAR)
val earliestYear = 1900 val earliestYear = 1900
@ -380,10 +378,10 @@ class GeneratorPlayer : FullScreenPlayer() {
) )
} }
dialog.subtitles_search.setOnQueryTextListener(object : binding.subtitlesSearch.setOnQueryTextListener(object :
androidx.appcompat.widget.SearchView.OnQueryTextListener { androidx.appcompat.widget.SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean { override fun onQueryTextSubmit(query: String?): Boolean {
dialog.search_loading_bar?.show() binding.searchLoadingBar.show()
ioSafe { ioSafe {
val search = val search =
AbstractSubtitleEntities.SubtitleSearch( AbstractSubtitleEntities.SubtitleSearch(
@ -415,7 +413,7 @@ class GeneratorPlayer : FullScreenPlayer() {
// ugly ik // ugly ik
activity?.runOnUiThread { activity?.runOnUiThread {
setSubtitlesList(items) 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 } val lang639_1 = languages.map { it.ISO_639_1 }
activity?.showDialog(languages.map { it.languageName }, activity?.showDialog(languages.map { it.languageName },
lang639_1.indexOf(currentLanguageTwoLetters), lang639_1.indexOf(currentLanguageTwoLetters),
@ -436,11 +434,11 @@ class GeneratorPlayer : FullScreenPlayer() {
true, true,
{ }) { index -> { }) { index ->
currentLanguageTwoLetters = lang639_1[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 -> currentSubtitle?.let { currentSubtitle ->
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api -> providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
ioSafe { ioSafe {
@ -466,7 +464,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
dialog.show() 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 //TODO: Set year text from currently loaded movie on Player
//dialog.subtitles_search_year?.setText(currentTempMeta.year) //dialog.subtitles_search_year?.setText(currentTempMeta.year)
} }
@ -509,7 +507,6 @@ class GeneratorPlayer : FullScreenPlayer() {
selectSourceDialog?.dismissSafe() selectSourceDialog?.dismissSafe()
showToast( showToast(
activity,
String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name), String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name),
Toast.LENGTH_LONG Toast.LENGTH_LONG
) )
@ -558,13 +555,15 @@ class GeneratorPlayer : FullScreenPlayer() {
val currentSubtitles = sortSubs(currentSubs) val currentSubtitles = sortSubs(currentSubs)
val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) 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 selectSourceDialog = sourceDialog
sourceDialog.show() sourceDialog.show()
val providerList = sourceDialog.sort_providers val providerList = binding.sortProviders
val subtitleList = sourceDialog.sort_subtitles val subtitleList = binding.sortSubtitles
val loadFromFileFooter: TextView = val loadFromFileFooter: TextView =
layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as 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) sourceDialog.dismissSafe(activity)
} }
fun setProfileName(profile: Int) { fun setProfileName(profile: Int) {
sourceDialog.source_settings_btt.setText( binding.sourceSettingsBtt.setText(
QualityDataHelper.getProfileName( QualityDataHelper.getProfileName(
profile profile
) )
@ -685,7 +684,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
setProfileName(currentQualityProfile) setProfileName(currentQualityProfile)
sourceDialog.profiles_click_settings.setOnClickListener { binding.profilesClickSettings.setOnClickListener {
val activity = activity ?: return@setOnClickListener val activity = activity ?: return@setOnClickListener
QualityProfileDialog( QualityProfileDialog(
activity, activity,
@ -699,7 +698,7 @@ class GeneratorPlayer : FullScreenPlayer() {
}.show() }.show()
} }
sourceDialog.subtitles_encoding_format?.apply { binding.subtitlesEncodingFormat.apply {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list) 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] text = prefNames[if (index == -1) 0 else index]
} }
sourceDialog.subtitles_click_settings?.setOnClickListener { binding.subtitlesClickSettings.setOnClickListener {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx) val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list) 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 var init = false
if (sourceIndex != startSource) { if (sourceIndex != startSource) {
init = true init = true
@ -781,18 +780,19 @@ class GeneratorPlayer : FullScreenPlayer() {
it.height?.times(-1) it.height?.times(-1)
} }
val currentAudioTracks = tracks.allAudioTracks val currentAudioTracks = tracks.allAudioTracks
val binding: PlayerSelectTracksBinding =
PlayerSelectTracksBinding.inflate(LayoutInflater.from(ctx), null, false)
val trackDialog = Dialog(ctx, R.style.AlertDialogCustomBlack) val trackDialog = Dialog(ctx, R.style.AlertDialogCustomBlack)
trackDialog.setContentView(R.layout.player_select_tracks) trackDialog.setContentView(binding.root)
trackDialog.show() trackDialog.show()
// selectTracksDialog = tracksDialog // selectTracksDialog = tracksDialog
val videosList = trackDialog.video_tracks_list val videosList = binding.videoTracksList
val audioList = trackDialog.auto_tracks_list val audioList = binding.autoTracksList
trackDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1 binding.videoTracksHolder.isVisible = currentVideoTracks.size > 1
trackDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1 binding.audioTracksHolder.isVisible = currentAudioTracks.size > 1
fun dismiss() { fun dismiss() {
if (isPlaying) { if (isPlaying) {
@ -857,11 +857,11 @@ class GeneratorPlayer : FullScreenPlayer() {
audioList.setItemChecked(which, true) audioList.setItemChecked(which, true)
} }
trackDialog.cancel_btt?.setOnClickListener { binding.cancelBtt.setOnClickListener {
trackDialog.dismissSafe(activity) trackDialog.dismissSafe(activity)
} }
trackDialog.apply_btt?.setOnClickListener { binding.applyBtt.setOnClickListener {
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart) val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
player.setPreferredAudioTrack( player.setPreferredAudioTrack(
currentTrack?.language, currentTrack?.id currentTrack?.language, currentTrack?.id
@ -889,7 +889,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
private fun noLinksFound() { 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() activity?.popCurrentPage()
} }
@ -1030,8 +1030,10 @@ class GeneratorPlayer : FullScreenPlayer() {
if (meta.tvType.isAnimeOp()) isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE 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) { if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) {
viewModel.preLoadNextLinks() viewModel.preLoadNextLinks()
@ -1168,7 +1170,7 @@ class GeneratorPlayer : FullScreenPlayer() {
//Hide title, if set in setting //Hide title, if set in setting
if (limitTitle < 0) { if (limitTitle < 0) {
player_video_title?.visibility = View.GONE playerBinding?.playerVideoTitle?.visibility = View.GONE
} else { } else {
//Truncate video title if it exceeds limit //Truncate video title if it exceeds limit
val differenceInLength = playerVideoTitle.length - limitTitle val differenceInLength = playerVideoTitle.length - limitTitle
@ -1179,8 +1181,8 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller
player_episode_filler_holder?.isVisible = isFiller ?: false playerBinding?.playerEpisodeFillerHolder?.isVisible = isFiller ?: false
player_video_title?.text = playerVideoTitle playerBinding?.playerVideoTitle?.text = playerVideoTitle
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@ -1201,8 +1203,10 @@ class GeneratorPlayer : FullScreenPlayer() {
3 -> "$source - $extra" 3 -> "$source - $extra"
else -> "" else -> ""
} }
player_video_title_rez?.text = title playerBinding?.playerVideoTitleRez?.apply {
player_video_title_rez?.isVisible = title.isNotBlank() text = title
isVisible = title.isNotBlank()
}
} }
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) { override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
@ -1230,7 +1234,14 @@ class GeneratorPlayer : FullScreenPlayer() {
unwrapBundle(savedInstanceState) unwrapBundle(savedInstanceState)
unwrapBundle(arguments) 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 var timestampShowState = false
@ -1243,7 +1254,7 @@ class GeneratorPlayer : FullScreenPlayer() {
skipIndex++ skipIndex++
println("displayTimeStamp = $show") println("displayTimeStamp = $show")
timestampShowState = show timestampShowState = show
skip_chapter_button?.apply { playerBinding?.skipChapterButton?.apply {
val showWidth = 170.toPx val showWidth = 170.toPx
val noShowWidth = 10.toPx val noShowWidth = 10.toPx
//if((show && width == showWidth) || (!show && width == noShowWidth)) { //if((show && width == showWidth) || (!show && width == noShowWidth)) {
@ -1263,7 +1274,7 @@ class GeneratorPlayer : FullScreenPlayer() {
from, to from, to
).apply { ).apply {
addListener(onEnd = { addListener(onEnd = {
if (!show) skip_chapter_button?.isVisible = false if (!show) playerBinding?.skipChapterButton?.isVisible = false
}) })
addUpdateListener { valueAnimator -> addUpdateListener { valueAnimator ->
val value = valueAnimator.animatedValue as Int val value = valueAnimator.animatedValue as Int
@ -1283,10 +1294,11 @@ class GeneratorPlayer : FullScreenPlayer() {
override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) { override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
if (timestamp != null) { if (timestamp != null) {
skip_chapter_button.setText(timestamp.uiText) println("timestamp: $timestamp")
playerBinding?.skipChapterButton?.setText(timestamp.uiText)
displayTimeStamp(true) displayTimeStamp(true)
val currentIndex = skipIndex val currentIndex = skipIndex
skip_chapter_button?.handler?.postDelayed({ playerBinding?.skipChapterButton?.handler?.postDelayed({
if (skipIndex == currentIndex) if (skipIndex == currentIndex)
displayTimeStamp(false) displayTimeStamp(false)
}, 6000) }, 6000)
@ -1329,11 +1341,11 @@ class GeneratorPlayer : FullScreenPlayer() {
viewModel.loadLinks() viewModel.loadLinks()
} }
overlay_loading_skip_button?.setOnClickListener { binding?.overlayLoadingSkipButton?.setOnClickListener {
startPlayer() startPlayer()
} }
player_loading_go_back?.setOnClickListener { binding?.playerLoadingGoBack?.setOnClickListener {
player.release() player.release()
activity?.popCurrentPage() activity?.popCurrentPage()
} }
@ -1357,7 +1369,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
is Resource.Failure -> { is Resource.Failure -> {
showToast(activity, it.errorString, Toast.LENGTH_LONG) showToast(it.errorString, Toast.LENGTH_LONG)
startPlayer() startPlayer()
} }
} }
@ -1366,8 +1378,8 @@ class GeneratorPlayer : FullScreenPlayer() {
observe(viewModel.currentLinks) { observe(viewModel.currentLinks) {
currentLinks = it currentLinks = it
val turnVisible = it.isNotEmpty() val turnVisible = it.isNotEmpty()
val wasGone = overlay_loading_skip_button?.isGone == true val wasGone = binding?.overlayLoadingSkipButton?.isGone == true
overlay_loading_skip_button?.isVisible = turnVisible binding?.overlayLoadingSkipButton?.isVisible = turnVisible
normalSafeApiCall { normalSafeApiCall {
if (currentLinks.any { link -> if (currentLinks.any { link ->
@ -1380,7 +1392,7 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
if (turnVisible && wasGone) { if (turnVisible && wasGone) {
overlay_loading_skip_button?.requestFocus() binding?.overlayLoadingSkipButton?.requestFocus()
} }
} }
@ -1410,4 +1422,4 @@ class GeneratorPlayer : FullScreenPlayer() {
} }
} }
} }
} }

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

View file

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

View file

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

View file

@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.player.source_priority
import android.app.Dialog import android.app.Dialog
import android.content.Context import android.content.Context
import android.view.View import android.view.LayoutInflater
import android.widget.EditText
import android.widget.TextView
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.appcompat.app.AlertDialog 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.R
import com.lagradost.cloudstream3.databinding.PlayerSelectSourcePriorityBinding
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlinx.android.synthetic.main.player_select_source_priority.*
class SourcePriorityDialog( class SourcePriorityDialog(
ctx: Context, val ctx: Context,
@StyleRes themeRes: Int, @StyleRes themeRes: Int,
val links: List<ExtractorLink>, val links: List<ExtractorLink>,
private val profile: QualityDataHelper.QualityProfile, private val profile: QualityDataHelper.QualityProfile,
@ -30,13 +24,14 @@ class SourcePriorityDialog(
private val updatedCallback: () -> Unit private val updatedCallback: () -> Unit
) : Dialog(ctx, themeRes) { ) : Dialog(ctx, themeRes) {
override fun show() { override fun show() {
setContentView(R.layout.player_select_source_priority) val binding = PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false)
val sourcesRecyclerView: RecyclerView = sort_sources setContentView(binding.root)
val qualitiesRecyclerView: RecyclerView = sort_qualities val sourcesRecyclerView = binding.sortSources
val profileText: EditText = profile_text_editable val qualitiesRecyclerView = binding.sortQualities
val saveBtt: View = save_btt val profileText = binding.profileTextEditable
val exitBtt: View = close_btt val saveBtt = binding.saveBtt
val helpBtt: View = help_btt val exitBtt = binding.closeBtt
val helpBtt = binding.helpBtt
profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context))
profileText.hint = txt(R.string.profile_number, 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.filterProviderByPreferredMedia
import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.QuickSearchBinding
import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe 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.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.quick_search.*
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
class QuickSearchFragment : Fragment() { class QuickSearchFragment : Fragment() {
@ -45,6 +46,13 @@ class QuickSearchFragment : Fragment() {
const val AUTOSEARCH_KEY = "autosearch" const val AUTOSEARCH_KEY = "autosearch"
const val PROVIDER_KEY = "providers" const val PROVIDER_KEY = "providers"
fun pushSearch(
autoSearch: String? = null,
providers: Array<String>? = null
) {
pushSearch(activity, autoSearch, providers)
}
fun pushSearch( fun pushSearch(
activity: Activity?, activity: Activity?,
autoSearch: String? = null, autoSearch: String? = null,
@ -72,6 +80,8 @@ class QuickSearchFragment : Fragment() {
private var providers: Set<String>? = null private var providers: Set<String>? = null
private lateinit var searchViewModel: SearchViewModel private lateinit var searchViewModel: SearchViewModel
var binding: QuickSearchBinding? = null
private var bottomSheetDialog: BottomSheetDialog? = null private var bottomSheetDialog: BottomSheetDialog? = null
@ -79,13 +89,21 @@ class QuickSearchFragment : Fragment() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
activity?.window?.setSoftInputMode( activity?.window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
) )
searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java] searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
bottomSheetDialog?.ownShow() 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() { override fun onDestroy() {
@ -111,7 +129,7 @@ class QuickSearchFragment : Fragment() {
activity?.getSpanCount()?.let { activity?.getSpanCount()?.let {
HomeFragment.currentSpan = it HomeFragment.currentSpan = it
} }
quick_search_autofit_results.spanCount = HomeFragment.currentSpan binding?.quickSearchAutofitResults?.spanCount = HomeFragment.currentSpan
HomeFragment.currentSpan = HomeFragment.currentSpan HomeFragment.currentSpan = HomeFragment.currentSpan
HomeFragment.configEvent.invoke(HomeFragment.currentSpan) HomeFragment.configEvent.invoke(HomeFragment.currentSpan)
} }
@ -123,7 +141,7 @@ class QuickSearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(quick_search_root) fixPaddingStatusbar(binding?.quickSearchRoot)
fixGrid() fixGrid()
arguments?.getStringArray(PROVIDER_KEY)?.let { arguments?.getStringArray(PROVIDER_KEY)?.let {
@ -136,23 +154,25 @@ class QuickSearchFragment : Fragment() {
} else false } else false
if (isSingleProvider) { if (isSingleProvider) {
quick_search_autofit_results.adapter = activity?.let { binding?.quickSearchAutofitResults?.apply {
SearchAdapter( adapter = SearchAdapter(
ArrayList(), ArrayList(),
quick_search_autofit_results, this,
) { callback -> ) { callback ->
SearchHelper.handleSearchClickCallback(activity, callback) SearchHelper.handleSearchClickCallback(callback)
} }
} }
try { 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) { } catch (e: Exception) {
logError(e) logError(e)
} }
} else { } else {
quick_search_master_recycler?.adapter = binding?.quickSearchMasterRecycler?.adapter =
ParentItemAdapter(mutableListOf(), { callback -> ParentItemAdapter(mutableListOf(), { callback ->
SearchHelper.handleSearchClickCallback(activity, callback) SearchHelper.handleSearchClickCallback(callback)
//when (callback.action) { //when (callback.action) {
//SEARCH_ACTION_LOAD -> { //SEARCH_ACTION_LOAD -> {
// clickCallback?.invoke(callback) // clickCallback?.invoke(callback)
@ -164,18 +184,17 @@ class QuickSearchFragment : Fragment() {
bottomSheetDialog = null bottomSheetDialog = null
}) })
}) })
quick_search_master_recycler?.layoutManager = GridLayoutManager(context, 1) binding?.quickSearchMasterRecycler?.layoutManager = GridLayoutManager(context, 1)
} }
binding?.quickSearchAutofitResults?.isVisible = isSingleProvider
quick_search_autofit_results?.isVisible = isSingleProvider binding?.quickSearchMasterRecycler?.isGone = isSingleProvider
quick_search_master_recycler?.isGone = isSingleProvider
val listLock = ReentrantLock() val listLock = ReentrantLock()
observe(searchViewModel.currentSearch) { list -> observe(searchViewModel.currentSearch) { list ->
try { try {
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
listLock.lock() listLock.lock()
(quick_search_master_recycler?.adapter as ParentItemAdapter?)?.apply { (binding?.quickSearchMasterRecycler?.adapter as ParentItemAdapter?)?.apply {
updateList(list.map { ongoing -> updateList(list.map { ongoing ->
val ongoingList = HomePageList( val ongoingList = HomePageList(
ongoing.apiName, ongoing.apiName,
@ -192,19 +211,18 @@ class QuickSearchFragment : Fragment() {
} }
val searchExitIcon = 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 = //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?.scaleX = 0.65f
//searchMagIcon?.scaleY = 0.65f //searchMagIcon?.scaleY = 0.65f
binding?.quickSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
quick_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
if (search(context, query, false)) if (search(context, query, false))
UIHelper.hideKeyboard(quick_search) UIHelper.hideKeyboard(binding?.quickSearch)
return true return true
} }
@ -214,27 +232,28 @@ class QuickSearchFragment : Fragment() {
return true return true
} }
}) })
binding?.quickSearchLoadingBar?.alpha = 0f
quick_search_loading_bar.alpha = 0f
observe(searchViewModel.searchResponse) { observe(searchViewModel.searchResponse) {
when (it) { when (it) {
is Resource.Success -> { is Resource.Success -> {
it.value.let { data -> it.value.let { data ->
(quick_search_autofit_results?.adapter as? SearchAdapter)?.updateList( (binding?.quickSearchAutofitResults?.adapter as? SearchAdapter)?.updateList(
context?.filterSearchResultByFilmQuality(data) ?: data context?.filterSearchResultByFilmQuality(data) ?: data
) )
} }
searchExitIcon?.alpha = 1f searchExitIcon?.alpha = 1f
quick_search_loading_bar?.alpha = 0f binding?.quickSearchLoadingBar?.alpha = 0f
} }
is Resource.Failure -> { is Resource.Failure -> {
// Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() // Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show()
searchExitIcon?.alpha = 1f searchExitIcon?.alpha = 1f
quick_search_loading_bar?.alpha = 0f binding?.quickSearchLoadingBar?.alpha = 0f
} }
is Resource.Loading -> { is Resource.Loading -> {
searchExitIcon?.alpha = 0f searchExitIcon?.alpha = 0f
quick_search_loading_bar?.alpha = 1f binding?.quickSearchLoadingBar?.alpha = 1f
} }
} }
} }
@ -246,13 +265,12 @@ class QuickSearchFragment : Fragment() {
// UIHelper.showInputMethod(view.findFocus()) // UIHelper.showInputMethod(view.findFocus())
// } // }
//} //}
binding?.quickSearchBack?.setOnClickListener {
quick_search_back.setOnClickListener {
activity?.popCurrentPage() activity?.popCurrentPage()
} }
arguments?.getString(AUTOSEARCH_KEY)?.let { arguments?.getString(AUTOSEARCH_KEY)?.let {
quick_search?.setQuery(it, true) binding?.quickSearch?.setQuery(it, true)
arguments?.remove(AUTOSEARCH_KEY) arguments?.remove(AUTOSEARCH_KEY)
} }
} }

View file

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

View file

@ -3,34 +3,24 @@ package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R 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.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.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.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage 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.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.* import java.util.*
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 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_PLAY_EPISODE_IN_MPV = 17
const val ACTION_MARK_AS_WATCHED = 18 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) data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
class EpisodeAdapter( class EpisodeAdapter(
@ -88,49 +79,10 @@ class EpisodeAdapter(
var cardList: MutableList<ResultEpisode> = mutableListOf() 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) { override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
if (holder.itemView.hasFocus()) { if (holder.itemView.hasFocus()) {
holder.itemView.clearFocus() 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>) { fun updateList(newList: List<ResultEpisode>) {
@ -144,27 +96,62 @@ class EpisodeAdapter(
diffResult.dispatchUpdatesTo(this) 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
/*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2) /*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2)
R.layout.result_episode_large R.layout.result_episode_large
else R.layout.result_episode*/ else R.layout.result_episode*/
return EpisodeCardViewHolder( return when (viewType) {
LayoutInflater.from(parent.context) 0 -> {
.inflate(layout, parent, false), EpisodeCardViewHolderSmall(
hasDownloadSupport, ResultEpisodeBinding.inflate(
clickCallback, LayoutInflater.from(parent.context),
downloadClickCallback 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) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is EpisodeCardViewHolder -> { is EpisodeCardViewHolderLarge -> {
holder.bind(cardList[position]) holder.bind(getItem(position))
mBoundViewHolders.add(holder) }
is EpisodeCardViewHolderSmall -> {
holder.bind(getItem(position))
} }
} }
} }
@ -173,94 +160,108 @@ class EpisodeAdapter(
return cardList.size return cardList.size
} }
class EpisodeCardViewHolder class EpisodeCardViewHolderLarge
constructor( constructor(
itemView: View, val binding: ResultEpisodeLargeBinding,
private val hasDownloadSupport: Boolean, private val hasDownloadSupport: Boolean,
private val clickCallback: (EpisodeClickEvent) -> Unit, private val clickCallback: (EpisodeClickEvent) -> Unit,
private val downloadClickCallback: (DownloadClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder { ) : RecyclerView.ViewHolder(binding.root) {
override var downloadButton = EasyDownloadButton()
var episodeDownloadBar: ContentLoadingProgressBar? = null
var episodeDownloadImage: ImageView? = null
var localCard: ResultEpisode? = null var localCard: ResultEpisode? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) { fun bind(card: ResultEpisode) {
localCard = card 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 isTrueTv = isTrueTvSettings()
val (parentView, otherView) = if (card.poster == null) { binding.apply {
itemView.episode_holder to itemView.episode_holder_large downloadButton.isVisible = hasDownloadSupport
} else { downloadButton.setDefaultClickListener(
itemView.episode_holder_large to itemView.episode_holder VideoDownloadHelper.DownloadEpisodeCached(
} card.name,
parentView.isVisible = true card.poster,
otherView.isVisible = false 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 DOWNLOAD_ACTION_LONG_CLICK -> {
val episodeFiller: MaterialButton? = parentView.episode_filler clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card))
val episodeRating: TextView? = parentView.episode_rating }
val episodeDescript: TextView? = parentView.episode_descript
val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress
val episodePoster: ImageView? = parentView.episode_poster
episodeDownloadBar = else -> {
parentView.result_episode_progress_downloaded downloadClickCallback.invoke(it)
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))
} }
episodePoster?.setOnLongClickListener { val name =
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
return@setOnLongClickListener true 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 { itemView.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
} }
@ -276,29 +277,30 @@ class EpisodeAdapter(
return@setOnLongClickListener true return@setOnLongClickListener true
} }
episodeDownloadImage?.isVisible = hasDownloadSupport //binding.resultEpisodeDownload.isVisible = hasDownloadSupport
episodeDownloadBar?.isVisible = hasDownloadSupport //binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport
reattachDownloadButton()
} }
}
override fun reattachDownloadButton() { class EpisodeCardViewHolderSmall
downloadButton.dispose() constructor(
val card = localCard val binding: ResultEpisodeBinding,
if (hasDownloadSupport && card != null) { private val hasDownloadSupport: Boolean,
if (episodeDownloadBar == null || private val clickCallback: (EpisodeClickEvent) -> Unit,
episodeDownloadImage == null private val downloadClickCallback: (DownloadClickEvent) -> Unit,
) return ) : RecyclerView.ViewHolder(binding.root) {
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings( @SuppressLint("SetTextI18n")
itemView.context, fun bind(card: ResultEpisode) {
card.id val isTrueTv = isTrueTvSettings()
)
downloadButton.setUpButton( binding.episodeHolder.layoutParams.apply {
downloadInfo?.fileLength, width =
downloadInfo?.totalBytes, if (isTvSettings()) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT
episodeDownloadBar ?: return, }
episodeDownloadImage ?: return,
null, binding.apply {
downloadButton.isVisible = hasDownloadSupport
downloadButton.setDefaultClickListener(
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
card.name, card.name,
card.poster, card.poster,
@ -309,14 +311,60 @@ class EpisodeAdapter(
card.rating, card.rating,
card.description, card.description,
System.currentTimeMillis(), System.currentTimeMillis(),
) ), null
) { ) {
if (it.action == DOWNLOAD_ACTION_DOWNLOAD) { when (it.action) {
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card)) DOWNLOAD_ACTION_DOWNLOAD -> {
} else { clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
downloadClickCallback.invoke(it) }
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 package com.lagradost.cloudstream3.ui.result
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
/* /*
@ -24,7 +23,6 @@ const val IMAGE_CLICK = 0
const val IMAGE_LONG_CLICK = 1 const val IMAGE_LONG_CLICK = 1
class ImageAdapter( class ImageAdapter(
val layout: Int,
val clickCallback: ((Int) -> Unit)? = null, val clickCallback: ((Int) -> Unit)? = null,
val nextFocusUp: Int? = null, val nextFocusUp: Int? = null,
val nextFocusDown: Int? = null, val nextFocusDown: Int? = null,
@ -34,7 +32,9 @@ class ImageAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ImageViewHolder( 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 class ImageViewHolder
constructor(itemView: View) : constructor(val binding: ResultMiniImageBinding) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
fun bind( fun bind(
img: Int, img: Int,
clickCallback: ((Int) -> Unit)?, clickCallback: ((Int) -> Unit)?,
nextFocusUp: Int?, nextFocusUp: Int?,
nextFocusDown: Int?, nextFocusDown: Int?,
) { ) {
(itemView as? ImageView?)?.apply { binding.root.apply {
setImageResource(img) setImageResource(img)
if (nextFocusDown != null) { if (nextFocusDown != null) {
this.nextFocusDownId = nextFocusDown this.nextFocusDownId = nextFocusDown

View file

@ -8,9 +8,10 @@ import com.lagradost.cloudstream3.mvvm.logError
fun RecyclerView?.setLinearListLayout(isHorizontal: Boolean = true) { fun RecyclerView?.setLinearListLayout(isHorizontal: Boolean = true) {
if (this == null) return if (this == null) return
this.layoutManager = this.layoutManager =
this.context?.let { LinearListLayout(it).apply { if (isHorizontal) setHorizontal() else setVertical() } } this.context?.let { LinearListLayout(it).apply { if (isHorizontal) setHorizontal() else setVertical() } }
?: this.layoutManager // ?: this.layoutManager
} }
open class LinearListLayout(context: Context?) : open class LinearListLayout(context: Context?) :
@ -66,7 +67,12 @@ open class LinearListLayout(context: Context?) :
(focused.parent as? RecyclerView)?.focusSearch(direction) (focused.parent as? RecyclerView)?.focusSearch(direction)
return null 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 { } else {
if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null if (direction == View.FOCUS_RIGHT || direction == View.FOCUS_LEFT) return null
if (direction == View.FOCUS_DOWN) 1 else -1 if (direction == View.FOCUS_DOWN) 1 else -1
@ -76,6 +82,13 @@ open class LinearListLayout(context: Context?) :
getPosition(getCorrectParent(focused))?.let { position -> getPosition(getCorrectParent(focused))?.let { position ->
val lookfor = dir + position val lookfor = dir + position
//clamp(dir + position, 0, recyclerView.adapter?.itemCount ?: return null) //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 { getViewFromPos(lookfor) ?: run {
scrollToPosition(lookfor) scrollToPosition(lookfor)
null null

View file

@ -1,33 +1,81 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.animation.Animator
import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View 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.isGone
import androidx.core.view.isVisible 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 androidx.recyclerview.widget.RecyclerView
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.ResourceSome import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
import com.lagradost.cloudstream3.mvvm.Some import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.observe 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.ExtractorLinkGenerator
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer 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.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper 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.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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.fragment_result_tv.*
class ResultFragmentTv : ResultFragment() { class ResultFragmentTv : Fragment() {
override val resultLayout = R.layout.fragment_result_tv 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() private var currentRecommendations: List<SearchResponse> = emptyList()
@ -36,12 +84,15 @@ class ResultFragmentTv : ResultFragment() {
is EpisodeRange -> { is EpisodeRange -> {
viewModel.changeRange(data) viewModel.changeRange(data)
} }
is Int -> { is Int -> {
viewModel.changeSeason(data) viewModel.changeSeason(data)
} }
is DubStatus -> { is DubStatus -> {
viewModel.changeDubStatus(data) viewModel.changeDubStatus(data)
} }
is String -> { is String -> {
setRecommendations(currentRecommendations, data) setRecommendations(currentRecommendations, data)
} }
@ -66,172 +117,640 @@ class ResultFragmentTv : ResultFragment() {
private fun hasNoFocus(): Boolean { private fun hasNoFocus(): Boolean {
val focus = activity?.currentFocus val focus = activity?.currentFocus
if (focus == null || !focus.isVisible) return true if (focus == null || !focus.isVisible) return true
return focus == this.result_root return focus == binding?.resultRoot
} }
override fun updateEpisodes(episodes: ResourceSome<List<ResultEpisode>>) { private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
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?) {
currentRecommendations = rec ?: emptyList() currentRecommendations = rec ?: emptyList()
val isInvalid = rec.isNullOrEmpty() val isInvalid = rec.isNullOrEmpty()
result_recommendations?.isGone = isInvalid binding?.apply {
result_recommendations_holder?.isGone = isInvalid resultRecommendationsList.isGone = isInvalid
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName resultRecommendationsHolder.isGone = isInvalid
(result_recommendations?.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst } val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
?: emptyList()) (resultRecommendationsList.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst }
?: emptyList())
rec?.map { it.apiName }?.distinct()?.let { apiNames -> rec?.map { it.apiName }?.distinct()?.let { apiNames ->
// very dirty selection // very dirty selection
result_recommendations_filter_selection?.isVisible = apiNames.size > 1 resultRecommendationsFilterSelection.isVisible = apiNames.size > 1
result_recommendations_filter_selection?.update(apiNames.map { txt(it) to it }) resultRecommendationsFilterSelection.update(apiNames.map { txt(it) to it })
result_recommendations_filter_selection?.select(apiNames.indexOf(matchAgainst)) resultRecommendationsFilterSelection.select(apiNames.indexOf(matchAgainst))
} ?: run { } ?: run {
result_recommendations_filter_selection?.isVisible = false resultRecommendationsFilterSelection.isVisible = false
}
} }
} }
var loadingDialog: Dialog? = null var loadingDialog: Dialog? = null
var popupDialog: 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
result_episodes?.layoutManager = // ===== setup =====
//LinearListLayout(result_episodes ?: return, result_episodes?.context).apply { val storedData = getStoredData() ?: return
LinearListLayout(result_episodes?.context).apply { activity?.window?.decorView?.clearFocus()
setHorizontal() 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() observeNullable(viewModel.resumeWatching) { resume ->
result_range_selection.setAdapter() binding?.apply {
result_dub_selection.setAdapter() // show progress no matter if series or movie
result_recommendations_filter_selection.setAdapter() 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 -> // if movie then hide both as movie button is
when (popup) { // always visible on movies, this is done in movie observe
is Some.Success -> {
popupDialog?.dismissSafe(activity)
popupDialog = activity?.let { act -> if (resume?.isMovie == true) {
val pop = popup.value resultPlaySeries.isVisible = false
val options = pop.getOptions(act) resultResumeSeries.isVisible = false
val title = pop.getTitle(act) return@observeNullable
}
act.showBottomDialogInstant( // if series then
options, title, { // > resultPlaySeries is visible when null
popupDialog = null // > resultResumeSeries is visible when not null
pop.callback(null) if (resume == null) {
}, { resultPlaySeries.isVisible = true
popupDialog = null resultResumeSeries.isVisible = false
pop.callback(it) 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)
) )
} }
} resultPlayMovie.setOnLongClickListener {
is Some.None -> { viewModel.handleAction(
popupDialog?.dismissSafe(activity) EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep)
popupDialog = null )
return@setOnLongClickListener true
}
if (hasNoFocus()) {
resultPlayMovie.requestFocus()
}
} }
} }
} }
observe(viewModel.loadedLinks) { load -> observeNullable(viewModel.selectPopup) { popup ->
when (load) { if (popup == null) {
is Some.Success -> { popupDialog?.dismissSafe(activity)
if (loadingDialog?.isShowing != true) { popupDialog = null
loadingDialog?.dismissSafe(activity) return@observeNullable
loadingDialog = null }
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 observeNullable(viewModel.loadedLinks) { load ->
viewModel.cancelLinks() if (load == null) {
} loadingDialog?.dismissSafe(activity)
//builder.setOnCancelListener { loadingDialog = null
// it?.dismiss() return@observeNullable
//} }
builder.setCanceledOnTouchOutside(true) if (loadingDialog?.isShowing != true) {
builder.show() loadingDialog?.dismissSafe(activity)
builder loadingDialog = null
} }
} loadingDialog = loadingDialog ?: context?.let { ctx ->
is Some.None -> { val builder = BottomSheetDialog(ctx)
loadingDialog?.dismissSafe(activity) builder.setContentView(R.layout.bottom_loading)
builder.setOnDismissListener {
loadingDialog = null loadingDialog = null
viewModel.cancelLinks()
} }
//builder.setOnCancelListener {
// it?.dismiss()
//}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
} }
} }
observe(viewModel.episodesCountText) { count -> observeNullable(viewModel.episodesCountText) { count ->
result_episodes_text.setText(count) binding?.resultEpisodesText.setText(count)
} }
observe(viewModel.selectedRangeIndex) { selected -> observe(viewModel.selectedRangeIndex) { selected ->
result_range_selection.select(selected) binding?.resultRangeSelection.select(selected)
} }
observe(viewModel.selectedSeasonIndex) { selected -> observe(viewModel.selectedSeasonIndex) { selected ->
result_season_selection.select(selected) binding?.resultSeasonSelection.select(selected)
} }
observe(viewModel.selectedDubStatusIndex) { selected -> observe(viewModel.selectedDubStatusIndex) { selected ->
result_dub_selection.select(selected) binding?.resultDubSelection.select(selected)
} }
observe(viewModel.rangeSelections) { observe(viewModel.rangeSelections) {
result_range_selection.update(it) binding?.resultRangeSelection.update(it)
} }
observe(viewModel.dubSubSelections) { observe(viewModel.dubSubSelections) {
result_dub_selection.update(it) binding?.resultDubSelection.update(it)
} }
observe(viewModel.seasonSelections) { observe(viewModel.seasonSelections) {
result_season_selection.update(it) binding?.resultSeasonSelection.update(it)
} }
observe(viewModel.recommendations) { recommendations ->
result_back?.setOnClickListener { setRecommendations(recommendations, null)
activity?.popCurrentPage()
} }
observe(viewModel.episodeSynopsis) { description ->
result_recommendations?.spanCount = 8 view.context?.let { ctx ->
result_recommendations?.adapter = val builder: AlertDialog.Builder =
SearchAdapter( AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
ArrayList(), builder.setMessage(description.html())
result_recommendations, .setTitle(R.string.synopsis)
) { callback -> .setOnDismissListener {
SearchHelper.handleSearchClickCallback(activity, callback) 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 android.widget.FrameLayout
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.discord.panels.PanelsChildGestureRegionObserver
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.IOnBackPressed 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(), open class ResultTrailerPlayer : ResultFragmentPhone(), IOnBackPressed {
PanelsChildGestureRegionObserver.GestureRegionsListener, IOnBackPressed {
override var lockRotation = false override var lockRotation = false
override var isFullScreenPlayer = false override var isFullScreenPlayer = false
@ -60,13 +52,13 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
screenHeight screenHeight
} }
result_trailer_loading?.isVisible = false //result_trailer_loading?.isVisible = false
result_smallscreen_holder?.isVisible = !isFullScreenPlayer resultBinding?.resultSmallscreenHolder?.isVisible = !isFullScreenPlayer
result_fullscreen_holder?.isVisible = isFullScreenPlayer binding?.resultFullscreenHolder?.isVisible = isFullScreenPlayer
val to = sw * h / w val to = sw * h / w
player_background?.apply { resultBinding?.fragmentTrailer?.playerBackground?.apply {
isVisible = true isVisible = true
layoutParams = layoutParams =
FrameLayout.LayoutParams( FrameLayout.LayoutParams(
@ -75,16 +67,17 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
) )
} }
player_intro_play?.apply { playerBinding?.playerIntroPlay?.apply {
layoutParams = layoutParams =
FrameLayout.LayoutParams( FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, 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) { if (playerBinding?.playerIntroPlay?.isGone == true) {
result_top_holder?.apply { resultBinding?.resultTopHolder?.apply {
val anim = ValueAnimator.ofInt( val anim = ValueAnimator.ofInt(
measuredHeight, measuredHeight,
@ -131,23 +124,30 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
private fun updateFullscreen(fullscreen: Boolean) { private fun updateFullscreen(fullscreen: Boolean) {
isFullScreenPlayer = fullscreen isFullScreenPlayer = fullscreen
lockRotation = 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) { if (fullscreen) {
enterFullscreen() enterFullscreen()
result_top_bar?.isVisible = false binding?.apply {
result_fullscreen_holder?.isVisible = true resultTopBar.isVisible = false
result_main_holder?.isVisible = false resultFullscreenHolder.isVisible = true
player_background?.let { view -> resultMainHolder.isVisible = false
(view.parent as ViewGroup?)?.removeView(view)
result_fullscreen_holder?.addView(view)
} }
} else {
result_top_bar?.isVisible = true resultBinding?.fragmentTrailer?.playerBackground?.let { view ->
result_fullscreen_holder?.isVisible = false
result_main_holder?.isVisible = true
player_background?.let { view ->
(view.parent as ViewGroup?)?.removeView(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() exitFullscreen()
} }
@ -157,14 +157,14 @@ open class ResultTrailerPlayer : com.lagradost.cloudstream3.ui.player.FullScreen
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
player_fullscreen?.setOnClickListener { playerBinding?.playerFullscreen?.setOnClickListener {
updateFullscreen(!isFullScreenPlayer) updateFullscreen(!isFullScreenPlayer)
} }
updateFullscreen(isFullScreenPlayer) updateFullscreen(isFullScreenPlayer)
uiReset() uiReset()
player_intro_play?.setOnClickListener { playerBinding?.playerIntroPlay?.setOnClickListener {
player_intro_play?.isGone = true playerBinding?.playerIntroPlay?.isGone = true
player.handleEvent(CSPlayerEvent.Play) player.handleEvent(CSPlayerEvent.Play)
updateUIVisibility() updateUIVisibility()
fixPlayerSize() fixPlayerSize()

View file

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

View file

@ -1,12 +1,11 @@
package com.lagradost.cloudstream3.ui.result package com.lagradost.cloudstream3.ui.result
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton 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 import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
typealias SelectData = Pair<UiText?, Any> 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return SelectViewHolder( 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 private class SelectViewHolder
constructor( constructor(
itemView: View, binding: ResultSelectionBinding,
) : ) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
private val item: MaterialButton = itemView as MaterialButton private val item: MaterialButton = binding.root
fun update(isSelected: Boolean) { fun update(isSelected: Boolean) {
item.isSelected = isSelected item.isSelected = isSelected

View file

@ -8,7 +8,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -162,11 +161,3 @@ fun TextView?.setTextHtml(text: UiText?) {
this.text = str.html() 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.SearchResponse 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.ui.AutofitRecyclerView
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.search_result_compact.view.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
/** Click */ /** Click */
@ -39,10 +38,23 @@ class SearchAdapter(
var hasNext: Boolean = false var hasNext: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val layout = 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( return CardViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false), layout,
clickCallback, clickCallback,
resView resView
) )
@ -73,20 +85,25 @@ class SearchAdapter(
class CardViewHolder class CardViewHolder
constructor( constructor(
itemView: View, val binding: ViewBinding,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
resView: AutofitRecyclerView resView: AutofitRecyclerView
) : ) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
val cardView: ImageView = itemView.imageView
private val compactView = false//itemView.context.getGridIsCompact() private val compactView = false//itemView.context.getGridIsCompact()
private val coverHeight: Int = private val coverHeight: Int =
if (compactView) 80.toPx else (resView.itemWidth / 0.68).roundToInt() 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) { fun bind(card: SearchResponse, position: Int) {
if (!compactView) { if (!compactView) {
cardView.apply { cardView?.apply {
layoutParams = FrameLayout.LayoutParams( layoutParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
coverHeight 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.removeKeys
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent 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.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe 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.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard 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 import java.util.concurrent.locks.ReentrantLock
const val SEARCH_PREF_TAGS = "search_pref_tags" const val SEARCH_PREF_TAGS = "search_pref_tags"
@ -89,6 +89,7 @@ class SearchFragment : Fragment() {
private val searchViewModel: SearchViewModel by activityViewModels() private val searchViewModel: SearchViewModel by activityViewModels()
private var bottomSheetDialog: BottomSheetDialog? = null private var bottomSheetDialog: BottomSheetDialog? = null
var binding: FragmentSearchBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -99,18 +100,21 @@ class SearchFragment : Fragment() {
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
) )
bottomSheetDialog?.ownShow() bottomSheetDialog?.ownShow()
return inflater.inflate(
if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search, val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search
container,
false val root = inflater.inflate(layout, container, false)
) // TODO TRYCATCH
binding = FragmentSearchBinding.bind(root)
return root
} }
private fun fixGrid() { private fun fixGrid() {
activity?.getSpanCount()?.let { activity?.getSpanCount()?.let {
currentSpan = it currentSpan = it
} }
search_autofit_results.spanCount = currentSpan binding?.searchAutofitResults?.spanCount = currentSpan
currentSpan = currentSpan currentSpan = currentSpan
HomeFragment.configEvent.invoke(currentSpan) HomeFragment.configEvent.invoke(currentSpan)
} }
@ -123,6 +127,7 @@ class SearchFragment : Fragment() {
override fun onDestroyView() { override fun onDestroyView() {
hideKeyboard() hideKeyboard()
bottomSheetDialog?.ownHide() bottomSheetDialog?.ownHide()
binding = null
super.onDestroyView() super.onDestroyView()
} }
@ -181,7 +186,7 @@ class SearchFragment : Fragment() {
searchViewModel.reloadRepos() searchViewModel.reloadRepos()
context?.filterProviderByPreferredMedia()?.let { validAPIs -> context?.filterProviderByPreferredMedia()?.let { validAPIs ->
bindChips( bindChips(
home_select_group, binding?.tvtypesChipsScroll?.tvtypesChips,
selectedSearchTypes, selectedSearchTypes,
validAPIs.flatMap { api -> api.supportedTypes }.distinct() validAPIs.flatMap { api -> api.supportedTypes }.distinct()
) { list -> ) { list ->
@ -189,7 +194,7 @@ class SearchFragment : Fragment() {
setKey(SEARCH_PREF_TAGS, selectedSearchTypes) setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
selectedSearchTypes.clear() selectedSearchTypes.clear()
selectedSearchTypes.addAll(list) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(searchRoot) fixPaddingStatusbar(binding?.searchRoot)
fixGrid() fixGrid()
reloadRepos() reloadRepos()
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let { binding?.apply {
SearchAdapter( val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? =
ArrayList(), SearchAdapter(
search_autofit_results, ArrayList(),
) { callback -> searchAutofitResults,
SearchHelper.handleSearchClickCallback(activity, callback) ) { callback ->
} SearchHelper.handleSearchClickCallback(callback)
}
searchAutofitResults.adapter = adapter
searchLoadingBar.alpha = 0f
} }
search_autofit_results.adapter = adapter
search_loading_bar.alpha = 0f
val searchExitIcon = 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 = // val searchMagIcon =
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon) // main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
//searchMagIcon.scaleX = 0.65f //searchMagIcon.scaleX = 0.65f
@ -230,7 +238,7 @@ class SearchFragment : Fragment() {
)!!.toMutableSet() )!!.toMutableSet()
} }
search_filter.setOnClickListener { searchView -> binding?.searchFilter?.setOnClickListener { searchView ->
searchView?.context?.let { ctx -> searchView?.context?.let { ctx ->
val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false) val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
var currentValidApis = listOf<MainAPI>() var currentValidApis = listOf<MainAPI>()
@ -241,7 +249,13 @@ class SearchFragment : Fragment() {
BottomSheetDialog(ctx) BottomSheetDialog(ctx)
builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED 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.show()
builder.let { dialog -> builder.let { dialog ->
val isMultiLang = ctx.getApiProviderLangSettings().let { set -> val isMultiLang = ctx.getApiProviderLangSettings().let { set ->
@ -303,7 +317,7 @@ class SearchFragment : Fragment() {
?: mutableListOf(TvType.Movie, TvType.TvSeries) ?: mutableListOf(TvType.Movie, TvType.TvSeries)
bindChips( bindChips(
dialog.home_select_group, binding.tvtypesChipsScroll.tvtypesChips,
selectedSearchTypes, selectedSearchTypes,
TvType.values().toList() TvType.values().toList()
) { list -> ) { list ->
@ -343,15 +357,15 @@ class SearchFragment : Fragment() {
?: mutableListOf(TvType.Movie, TvType.TvSeries) ?: mutableListOf(TvType.Movie, TvType.TvSeries)
if (isTrueTvSettings()) { if (isTrueTvSettings()) {
search_filter.isFocusable = true binding?.searchFilter?.isFocusable = true
search_filter.isFocusableInTouchMode = true binding?.searchFilter?.isFocusableInTouchMode = true
} }
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener { binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean { override fun onQueryTextSubmit(query: String): Boolean {
search(query) search(query)
main_search?.let { binding?.mainSearch?.let {
hideKeyboard(it) hideKeyboard(it)
} }
@ -365,17 +379,17 @@ class SearchFragment : Fragment() {
searchViewModel.clearSearch() searchViewModel.clearSearch()
searchViewModel.updateHistory() searchViewModel.updateHistory()
} }
binding?.apply {
search_history_holder?.isVisible = showHistory searchHistoryHolder.isVisible = showHistory
searchMasterRecycler.isVisible = !showHistory && isAdvancedSearch
search_master_recycler?.isVisible = !showHistory && isAdvancedSearch searchAutofitResults.isVisible = !showHistory && !isAdvancedSearch
search_autofit_results?.isVisible = !showHistory && !isAdvancedSearch }
return true return true
} }
}) })
search_clear_call_history?.setOnClickListener { binding?.searchClearCallHistory?.setOnClickListener {
activity?.let { ctx -> activity?.let { ctx ->
val builder: AlertDialog.Builder = AlertDialog.Builder(ctx) val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
val dialogClickListener = val dialogClickListener =
@ -409,8 +423,8 @@ class SearchFragment : Fragment() {
} }
observe(searchViewModel.currentHistory) { list -> observe(searchViewModel.currentHistory) { list ->
search_clear_call_history?.isVisible = list.isNotEmpty() binding?.searchClearCallHistory?.isVisible = list.isNotEmpty()
(search_history_recycler.adapter as? SearchHistoryAdaptor?)?.updateList(list) (binding?.searchHistoryRecycler?.adapter as? SearchHistoryAdaptor?)?.updateList(list)
} }
searchViewModel.updateHistory() searchViewModel.updateHistory()
@ -420,20 +434,20 @@ class SearchFragment : Fragment() {
is Resource.Success -> { is Resource.Success -> {
it.value.let { data -> it.value.let { data ->
if (data.isNotEmpty()) { if (data.isNotEmpty()) {
(search_autofit_results?.adapter as? SearchAdapter)?.updateList(data) (binding?.searchAutofitResults?.adapter as? SearchAdapter)?.updateList(data)
} }
} }
searchExitIcon.alpha = 1f searchExitIcon?.alpha = 1f
search_loading_bar.alpha = 0f binding?.searchLoadingBar?.alpha = 0f
} }
is Resource.Failure -> { is Resource.Failure -> {
// Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show() // Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show()
searchExitIcon.alpha = 1f searchExitIcon?.alpha = 1f
search_loading_bar.alpha = 0f binding?.searchLoadingBar?.alpha = 0f
} }
is Resource.Loading -> { is Resource.Loading -> {
searchExitIcon.alpha = 0f searchExitIcon?.alpha = 0f
search_loading_bar.alpha = 1f binding?.searchLoadingBar?.alpha = 1f
} }
} }
} }
@ -443,7 +457,7 @@ class SearchFragment : Fragment() {
try { try {
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist // https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
listLock.lock() listLock.lock()
(search_master_recycler?.adapter as ParentItemAdapter?)?.apply { (binding?.searchMasterRecycler?.adapter as ParentItemAdapter?)?.apply {
val newItems = list.map { ongoing -> val newItems = list.map { ongoing ->
val dataList = val dataList =
if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList() if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList()
@ -477,7 +491,7 @@ class SearchFragment : Fragment() {
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
ParentItemAdapter(mutableListOf(), { callback -> ParentItemAdapter(mutableListOf(), { callback ->
SearchHelper.handleSearchClickCallback(activity, callback) SearchHelper.handleSearchClickCallback(callback)
}, { item -> }, { item ->
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = { bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
bottomSheetDialog = null bottomSheetDialog = null
@ -490,8 +504,8 @@ class SearchFragment : Fragment() {
SEARCH_HISTORY_OPEN -> { SEARCH_HISTORY_OPEN -> {
searchViewModel.clearSearch() searchViewModel.clearSearch()
if (searchItem.type.isNotEmpty()) if (searchItem.type.isNotEmpty())
updateChips(home_select_group, searchItem.type.toMutableList()) updateChips(binding?.tvtypesChipsScroll?.tvtypesChips, searchItem.type.toMutableList())
main_search?.setQuery(searchItem.searchText, true) binding?.mainSearch?.setQuery(searchItem.searchText, true)
} }
SEARCH_HISTORY_REMOVE -> { SEARCH_HISTORY_REMOVE -> {
removeKey(SEARCH_HISTORY_KEY, searchItem.key) removeKey(SEARCH_HISTORY_KEY, searchItem.key)
@ -503,20 +517,23 @@ class SearchFragment : Fragment() {
} }
} }
search_history_recycler?.adapter = historyAdapter binding?.apply {
search_history_recycler?.layoutManager = GridLayoutManager(context, 1) searchHistoryRecycler.adapter = historyAdapter
searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1)
search_master_recycler?.adapter = masterAdapter searchMasterRecycler.adapter = masterAdapter
search_master_recycler?.layoutManager = GridLayoutManager(context, 1) searchMasterRecycler.layoutManager = GridLayoutManager(context, 1)
// Automatically search the specified query, this allows the app search to launch from intent // Automatically search the specified query, this allows the app search to launch from intent
arguments?.getString(SEARCH_QUERY)?.let { query -> arguments?.getString(SEARCH_QUERY)?.let { query ->
if (query.isBlank()) return@let if (query.isBlank()) return@let
main_search?.setQuery(query, true) mainSearch.setQuery(query, true)
// Clear the query as to not make it request the same query every time the page is opened // Clear the query as to not make it request the same query every time the page is opened
arguments?.putString(SEARCH_QUERY, null) arguments?.putString(SEARCH_QUERY, null)
}
} }
// SubtitlesFragment.push(activity) // SubtitlesFragment.push(activity)
//searchViewModel.search("iron man") //searchViewModel.search("iron man")
//(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro") //(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.app.Activity
import android.widget.Toast import android.widget.Toast
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
@ -15,21 +16,21 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
object SearchHelper { object SearchHelper {
fun handleSearchClickCallback(activity: Activity?, callback: SearchClickCallback) { fun handleSearchClickCallback(callback: SearchClickCallback) {
val card = callback.card val card = callback.card
when (callback.action) { when (callback.action) {
SEARCH_ACTION_LOAD -> { SEARCH_ACTION_LOAD -> {
activity.loadSearchResult(card) loadSearchResult(card)
} }
SEARCH_ACTION_PLAY_FILE -> { SEARCH_ACTION_PLAY_FILE -> {
if (card is DataStoreHelper.ResumeWatchingResult) { if (card is DataStoreHelper.ResumeWatchingResult) {
val id = card.id val id = card.id
if(id == null) { if(id == null) {
showToast(activity, R.string.error_invalid_id, Toast.LENGTH_SHORT) showToast(R.string.error_invalid_id, Toast.LENGTH_SHORT)
} else { } else {
if (card.isFromDownload) { if (card.isFromDownload) {
handleDownloadClick( handleDownloadClick(
activity, DownloadClickEvent( DownloadClickEvent(
DOWNLOAD_ACTION_PLAY_FILE, DOWNLOAD_ACTION_PLAY_FILE,
VideoDownloadHelper.DownloadEpisodeCached( VideoDownloadHelper.DownloadEpisodeCached(
card.name, card.name,
@ -45,12 +46,11 @@ object SearchHelper {
) )
) )
} else { } else {
activity.loadSearchResult(card, START_ACTION_LOAD_EP, id) loadSearchResult(card, START_ACTION_LOAD_EP, id)
} }
} }
} else { } else {
handleSearchClickCallback( handleSearchClickCallback(
activity,
SearchClickCallback(SEARCH_ACTION_LOAD, callback.view, -1, callback.card) SearchClickCallback(SEARCH_ACTION_LOAD, callback.view, -1, callback.card)
) )
} }
@ -60,10 +60,10 @@ object SearchHelper {
(activity as? MainActivity?)?.apply { (activity as? MainActivity?)?.apply {
loadPopup(callback.card) loadPopup(callback.card)
} ?: kotlin.run { } ?: kotlin.run {
showToast(activity, callback.card.name, Toast.LENGTH_SHORT) showToast(callback.card.name, Toast.LENGTH_SHORT)
} }
} else { } 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.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType 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( data class SearchHistoryItem(
@JsonProperty("searchedAt") val searchedAt: Long, @JsonProperty("searchedAt") val searchedAt: Long,
@ -34,8 +35,7 @@ class SearchHistoryAdaptor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder( return CardViewHolder(
LayoutInflater.from(parent.context) SearchHistoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
.inflate(R.layout.search_history_item, parent, false),
clickCallback, clickCallback,
) )
} }
@ -65,22 +65,24 @@ class SearchHistoryAdaptor(
class CardViewHolder class CardViewHolder
constructor( constructor(
itemView: View, val binding: SearchHistoryItemBinding,
private val clickCallback: (SearchHistoryCallback) -> Unit, private val clickCallback: (SearchHistoryCallback) -> Unit,
) : ) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
private val removeButton: ImageView = itemView.home_history_remove // private val removeButton: ImageView = itemView.home_history_remove
private val openButton: View = itemView.home_history_tab // private val openButton: View = itemView.home_history_tab
private val title: TextView = itemView.home_history_title // private val title: TextView = itemView.home_history_title
fun bind(card: SearchHistoryItem) { fun bind(card: SearchHistoryItem) {
title.text = card.searchText binding.apply {
homeHistoryTitle.text = card.searchText
removeButton.setOnClickListener { homeHistoryRemove.setOnClickListener {
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE)) clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE))
} }
openButton.setOnClickListener { homeHistoryTab.setOnClickListener {
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN)) clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN))
}
} }
} }
} }

View file

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

View file

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

View file

@ -3,11 +3,10 @@ package com.lagradost.cloudstream3.ui.settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.AccountSingleBinding
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.utils.UIHelper.setImage 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( class AccountAdapter(
val cardList: List<AuthAPI.LoginInfo>, val cardList: List<AuthAPI.LoginInfo>,
val layout: Int = R.layout.account_single,
private val clickCallback: (AccountClickCallback) -> Unit private val clickCallback: (AccountClickCallback) -> Unit
) : ) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder( 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 class CardViewHolder
constructor(itemView: View, private val clickCallback: (AccountClickCallback) -> Unit) : constructor(val binding: AccountSingleBinding, private val clickCallback: (AccountClickCallback) -> Unit) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!! // private val pfp: ImageView = itemView.findViewById(R.id.account_profile_picture)!!
private val accountName: TextView = itemView.findViewById(R.id.account_name)!! // private val accountName: TextView = itemView.findViewById(R.id.account_name)!!
fun bind(card: AuthAPI.LoginInfo) { fun bind(card: AuthAPI.LoginInfo) {
// just in case name is null account index will show, should never happened // just in case name is null account index will show, should never happened
accountName.text = card.name ?: "%s %d".format( binding.accountName.text = card.name ?: "%s %d".format(
accountName.context.getString(R.string.account), binding.accountName.context.getString(R.string.account),
card.accountIndex card.accountIndex
) )
pfp.isVisible = pfp.setImage(card.profilePicture) binding.accountProfilePicture.isVisible = binding.accountProfilePicture.setImage(card.profilePicture)
itemView.setOnClickListener { itemView.setOnClickListener {
clickCallback.invoke(AccountClickCallback(0, itemView, card)) 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.AcraApplication.Companion.openBrowser
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R 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.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.AccountManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.aniListApi 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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.setImage 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() { class SettingsAccount : PreferenceFragmentCompat() {
companion object { companion object {
@ -43,15 +43,18 @@ class SettingsAccount : PreferenceFragmentCompat() {
api: AccountManager, api: AccountManager,
info: AuthAPI.LoginInfo info: AuthAPI.LoginInfo
) { ) {
if (activity == null) return
val binding: AccountManagmentBinding =
AccountManagmentBinding.inflate(activity.layoutInflater, null, false)
val builder = val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(activity, R.style.AlertDialogCustom)
.setView(R.layout.account_managment) .setView(binding.root)
val dialog = builder.show() val dialog = builder.show()
dialog.account_main_profile_picture_holder?.isVisible = binding.accountMainProfilePictureHolder.isVisible =
dialog.account_main_profile_picture?.setImage(info.profilePicture) == true binding.accountMainProfilePicture.setImage(info.profilePicture)
dialog.account_logout?.setOnClickListener { binding.accountLogout.setOnClickListener {
api.logOut() api.logOut()
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
@ -60,26 +63,28 @@ class SettingsAccount : PreferenceFragmentCompat() {
dialog.findViewById<TextView>(R.id.account_name)?.text = it dialog.findViewById<TextView>(R.id.account_name)?.text = it
} }
dialog.account_site?.text = api.name binding.accountSite.text = api.name
dialog.account_switch_account?.setOnClickListener { binding.accountSwitchAccount.setOnClickListener {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
showAccountSwitch(activity, api) showAccountSwitch(activity, api)
} }
if (isTvSettings()) { 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 accounts = api.getAccounts() ?: return
val binding: AccountSwitchBinding =
AccountSwitchBinding.inflate(activity.layoutInflater, null, false)
val builder = val builder =
AlertDialog.Builder(activity, R.style.AlertDialogCustom) AlertDialog.Builder(activity, R.style.AlertDialogCustom)
.setView(R.layout.account_switch) .setView(binding.root)
val dialog = builder.show() val dialog = builder.show()
dialog.account_add?.setOnClickListener { binding.accountAdd.setOnClickListener {
addAccount(activity, api) addAccount(activity, api)
dialog?.dismissSafe(activity) dialog?.dismissSafe(activity)
} }
@ -96,7 +101,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
} }
api.accountIndex = ogIndex api.accountIndex = ogIndex
val adapter = AccountAdapter(items, R.layout.account_single) { val adapter = AccountAdapter(items) {
dialog?.dismissSafe(activity) dialog?.dismissSafe(activity)
api.changeAccount(it.card.accountIndex) api.changeAccount(it.card.accountIndex)
} }
@ -111,17 +116,21 @@ class SettingsAccount : PreferenceFragmentCompat() {
is OAuth2API -> { is OAuth2API -> {
api.authenticate(activity) api.authenticate(activity)
} }
is InAppAuthAPI -> { is InAppAuthAPI -> {
if (activity == null) return
val binding: AddAccountInputBinding =
AddAccountInputBinding.inflate(activity.layoutInflater, null, false)
val builder = val builder =
AlertDialog.Builder(activity ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(activity, R.style.AlertDialogCustom)
.setView(R.layout.add_account_input) .setView(binding.root)
val dialog = builder.show() val dialog = builder.show()
val visibilityMap = mapOf( val visibilityMap = listOf(
dialog.login_email_input to api.requiresEmail, binding.loginEmailInput to api.requiresEmail,
dialog.login_password_input to api.requiresPassword, binding.loginPasswordInput to api.requiresPassword,
dialog.login_server_input to api.requiresServer, binding.loginServerInput to api.requiresServer,
dialog.login_username_input to api.requiresUsername binding.loginUsernameInput to api.requiresUsername
) )
if (isTvSettings()) { if (isTvSettings()) {
@ -145,12 +154,12 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
} }
dialog.login_email_input?.isVisible = api.requiresEmail binding.loginEmailInput.isVisible = api.requiresEmail
dialog.login_password_input?.isVisible = api.requiresPassword binding.loginPasswordInput.isVisible = api.requiresPassword
dialog.login_server_input?.isVisible = api.requiresServer binding.loginServerInput.isVisible = api.requiresServer
dialog.login_username_input?.isVisible = api.requiresUsername binding.loginUsernameInput.isVisible = api.requiresUsername
dialog.create_account?.isGone = api.createAccountUrl.isNullOrBlank() binding.createAccount.isGone = api.createAccountUrl.isNullOrBlank()
dialog.create_account?.setOnClickListener { binding.createAccount.setOnClickListener {
openBrowser( openBrowser(
api.createAccountUrl ?: return@setOnClickListener, api.createAccountUrl ?: return@setOnClickListener,
activity activity
@ -159,43 +168,43 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
val displayedItems = listOf( val displayedItems = listOf(
dialog.login_username_input, binding.loginUsernameInput,
dialog.login_email_input, binding.loginEmailInput,
dialog.login_server_input, binding.loginServerInput,
dialog.login_password_input binding.loginPasswordInput
).filter { it.isVisible } ).filter { it.isVisible }
displayedItems.foldRight(displayedItems.firstOrNull()) { item, previous -> displayedItems.foldRight(displayedItems.firstOrNull()) { item, previous ->
item?.id?.let { previous?.nextFocusDownId = it } item.id.let { previous?.nextFocusDownId = it }
previous?.id?.let { item?.nextFocusUpId = it } previous?.id?.let { item.nextFocusUpId = it }
item item
} }
displayedItems.firstOrNull()?.let { displayedItems.firstOrNull()?.let {
dialog.create_account?.nextFocusDownId = it.id binding.createAccount.nextFocusDownId = it.id
it.nextFocusUpId = dialog.create_account.id it.nextFocusUpId = binding.createAccount.id
} }
dialog.apply_btt?.id?.let { binding.applyBtt.id.let {
displayedItems.lastOrNull()?.nextFocusDownId = it displayedItems.lastOrNull()?.nextFocusDownId = it
} }
dialog.text1?.text = api.name binding.text1.text = api.name
if (api.storesPasswordInPlainText) { if (api.storesPasswordInPlainText) {
api.getLatestLoginData()?.let { data -> api.getLatestLoginData()?.let { data ->
dialog.login_email_input?.setText(data.email ?: "") binding.loginEmailInput.setText(data.email ?: "")
dialog.login_server_input?.setText(data.server ?: "") binding.loginServerInput.setText(data.server ?: "")
dialog.login_username_input?.setText(data.username ?: "") binding.loginUsernameInput.setText(data.username ?: "")
dialog.login_password_input?.setText(data.password ?: "") binding.loginPasswordInput.setText(data.password ?: "")
} }
} }
dialog.apply_btt?.setOnClickListener { binding.applyBtt.setOnClickListener {
val loginData = InAppAuthAPI.LoginData( val loginData = InAppAuthAPI.LoginData(
username = if (api.requiresUsername) dialog.login_username_input?.text?.toString() else null, username = if (api.requiresUsername) binding.loginUsernameInput.text?.toString() else null,
password = if (api.requiresPassword) dialog.login_password_input?.text?.toString() else null, password = if (api.requiresPassword) binding.loginPasswordInput.text?.toString() else null,
email = if (api.requiresEmail) dialog.login_email_input?.text?.toString() else null, email = if (api.requiresEmail) binding.loginEmailInput.text?.toString() else null,
server = if (api.requiresServer) dialog.login_server_input?.text?.toString() else null, server = if (api.requiresServer) binding.loginServerInput.text?.toString() else null,
) )
ioSafe { ioSafe {
val isSuccessful = try { val isSuccessful = try {
@ -207,7 +216,6 @@ class SettingsAccount : PreferenceFragmentCompat() {
activity.runOnUiThread { activity.runOnUiThread {
try { try {
showToast( showToast(
activity,
activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail) activity.getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail)
.format( .format(
api.name api.name
@ -220,10 +228,11 @@ class SettingsAccount : PreferenceFragmentCompat() {
} }
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
dialog.cancel_btt?.setOnClickListener { binding.cancelBtt.setOnClickListener {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
} }
else -> { else -> {
throw NotImplementedError("You are trying to add an account that has an unknown login method") 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.appbar.MaterialToolbar
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment 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.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx 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 import java.io.File
class SettingsFragment : Fragment() { class SettingsFragment : Fragment() {
companion object { companion object {
var beneneCount = 0 var beneneCount = 0
private var isTv : Boolean = false private var isTv: Boolean = false
private var isTrueTv : Boolean = false private var isTrueTv: Boolean = false
fun PreferenceFragmentCompat?.getPref(id: Int): Preference? { fun PreferenceFragmentCompat?.getPref(id: Int): Preference? {
if (this == null) return null if (this == null) return null
@ -55,26 +57,31 @@ class SettingsFragment : Fragment() {
fun Fragment?.setUpToolbar(title: String) { fun Fragment?.setUpToolbar(title: String) {
if (this == null) return if (this == null) return
settings_toolbar?.apply { val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
settingsToolbar.apply {
setTitle(title) setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
setNavigationOnClickListener { setNavigationOnClickListener {
activity?.onBackPressed() activity?.onBackPressed()
} }
} }
context.fixPaddingStatusbar(settings_toolbar) fixPaddingStatusbar(settingsToolbar)
} }
fun Fragment?.setUpToolbar(@StringRes title: Int) { fun Fragment?.setUpToolbar(@StringRes title: Int) {
if (this == null) return if (this == null) return
settings_toolbar?.apply { val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
settingsToolbar.apply {
setTitle(title) setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
setNavigationOnClickListener { setNavigationOnClickListener {
activity?.onBackPressed() activity?.onBackPressed()
} }
} }
context.fixPaddingStatusbar(settings_toolbar) fixPaddingStatusbar(settingsToolbar)
} }
fun getFolderSize(dir: File): Long { 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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
): View? { ): View {
return inflater.inflate(R.layout.main_settings, container, false) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -152,41 +168,41 @@ class SettingsFragment : Fragment() {
activity?.navigate(id, Bundle()) activity?.navigate(id, Bundle())
} }
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
val isTrueTv = isTrueTvSettings() val isTrueTv = isTrueTvSettings()
for (syncApi in accountManagers) { for (syncApi in accountManagers) {
val login = syncApi.loginInfo() val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue val pic = login?.profilePicture ?: continue
if (settings_profile_pic?.setImage( if (binding?.settingsProfilePic?.setImage(
pic, pic,
errorImageDrawable = HomeFragment.errorProfilePic errorImageDrawable = HomeFragment.errorProfilePic
) == true ) == true
) { ) {
settings_profile_text?.text = login.name binding?.settingsProfileText?.text = login.name
settings_profile?.isVisible = true binding?.settingsProfile?.isVisible = true
break break
} }
} }
binding?.apply {
listOf( listOf(
Pair(settings_general, R.id.action_navigation_settings_to_navigation_settings_general), settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general,
Pair(settings_player, R.id.action_navigation_settings_to_navigation_settings_player), settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player,
Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account), settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account,
Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui), settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui,
Pair(settings_providers, R.id.action_navigation_settings_to_navigation_settings_providers), settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers,
Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates), settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates,
Pair( settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions,
settings_extensions, ).forEach { (view, navigationId) ->
R.id.action_navigation_settings_to_navigation_settings_extensions view.apply {
), setOnClickListener {
).forEach { (view, navigationId) -> navigate(navigationId)
view?.apply { }
setOnClickListener { if (isTrueTv) {
navigate(navigationId) isFocusable = true
} isFocusableInTouchMode = true
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.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app 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.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient 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.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath 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 import java.io.File
fun getCurrentLocale(context: Context): String { fun getCurrentLocale(context: Context): String {
@ -69,6 +70,7 @@ val appLanguages = arrayListOf(
Triple("", "español", "es"), Triple("", "español", "es"),
Triple("", "فارسی", "fa"), Triple("", "فارسی", "fa"),
Triple("", "français", "fr"), Triple("", "français", "fr"),
Triple("", "galego", "gl"),
Triple("", "हिन्दी", "hi"), Triple("", "हिन्दी", "hi"),
Triple("", "hrvatski", "hr"), Triple("", "hrvatski", "hr"),
Triple("", "magyar", "hu"), Triple("", "magyar", "hu"),
@ -188,7 +190,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
fun showAdd() { fun showAdd() {
val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name } val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog( activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" }, providers.map { "${it.name} (${it.mainUrl})" },
-1, -1,
@ -197,21 +199,23 @@ class SettingsGeneral : PreferenceFragmentCompat() {
{}) { selection -> {}) { selection ->
val provider = providers.getOrNull(selection) ?: return@showDialog val provider = providers.getOrNull(selection) ?: return@showDialog
val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(layoutInflater,null,false)
val builder = val builder =
AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom) AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom)
.setView(R.layout.add_site_input) .setView(binding.root)
val dialog = builder.create() val dialog = builder.create()
dialog.show() dialog.show()
dialog.text2?.text = provider.name binding.text2.text = provider.name
dialog.apply_btt?.setOnClickListener { binding.applyBtt.setOnClickListener {
val name = dialog.site_name_input?.text?.toString() val name = binding.siteNameInput.text?.toString()
val url = dialog.site_url_input?.text?.toString() val url = binding.siteUrlInput.text?.toString()
val lang = dialog.site_lang_input?.text?.toString() val lang = binding.siteLangInput.text?.toString()
val realLang = if (lang.isNullOrBlank()) provider.lang else lang val realLang = if (lang.isNullOrBlank()) provider.lang else lang
if (url.isNullOrBlank() || name.isNullOrBlank() || realLang.length != 2) { 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 return@setOnClickListener
} }
@ -219,10 +223,12 @@ class SettingsGeneral : PreferenceFragmentCompat() {
val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang) val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang)
current.add(newSite) current.add(newSite)
setKey(USER_PROVIDER_API, current.toTypedArray()) setKey(USER_PROVIDER_API, current.toTypedArray())
// reload apis
MainActivity.afterPluginsLoadedEvent.invoke(false)
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
dialog.cancel_btt?.setOnClickListener { binding.cancelBtt.setOnClickListener {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
} }
@ -242,18 +248,19 @@ class SettingsGeneral : PreferenceFragmentCompat() {
} }
fun showAddOrDelete() { fun showAddOrDelete() {
val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate(layoutInflater,null,false)
val builder = val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom) AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.add_remove_sites) .setView(binding.root)
val dialog = builder.create() val dialog = builder.create()
dialog.show() dialog.show()
dialog.add_site?.setOnClickListener { binding.addSite.setOnClickListener {
showAdd() showAdd()
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
dialog.remove_site?.setOnClickListener { binding.removeSite.setOnClickListener {
showDelete() showDelete()
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }

View file

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

View file

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

View file

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

View file

@ -7,7 +7,6 @@ import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.amap import com.lagradost.cloudstream3.amap
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline import com.lagradost.cloudstream3.plugins.PluginManager.getPluginsOnline
@ -40,8 +39,8 @@ class ExtensionsViewModel : ViewModel() {
private val _repositories = MutableLiveData<Array<RepositoryData>>() private val _repositories = MutableLiveData<Array<RepositoryData>>()
val repositories: LiveData<Array<RepositoryData>> = _repositories val repositories: LiveData<Array<RepositoryData>> = _repositories
private val _pluginStats: MutableLiveData<Some<PluginStats>> = MutableLiveData(Some.None) private val _pluginStats: MutableLiveData<PluginStats?> = MutableLiveData(null)
val pluginStats: LiveData<Some<PluginStats>> = _pluginStats val pluginStats: LiveData<PluginStats?> = _pluginStats
//TODO CACHE GET REQUESTS //TODO CACHE GET REQUESTS
// DO not use viewModelScope.launchSafe, it will ANR on slow internet // 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 }) { debugAssert({ stats.downloaded + stats.notDownloaded + stats.disabled != stats.total }) {
"downloaded(${stats.downloaded}) + notDownloaded(${stats.notDownloaded}) + disabled(${stats.disabled}) != total(${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) 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.text.format.Formatter.formatShortFileSize
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone 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.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.ui.result.setText 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.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
import kotlinx.android.synthetic.main.repository_item.view.*
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import java.text.DecimalFormat import java.text.DecimalFormat
import kotlin.math.floor
import kotlin.math.log10
data class PluginViewData( data class PluginViewData(
@ -45,8 +46,10 @@ class PluginAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 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()) R.layout.repository_item_tv else R.layout.repository_item
val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return PluginViewHolder( 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 // Clear glide image because setImageResource doesn't override
override fun onViewRecycled(holder: RecyclerView.ViewHolder) { override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
holder.itemView.entry_icon?.let { pluginIcon -> if (holder is PluginViewHolder) {
GlideApp.with(pluginIcon).clear(pluginIcon) holder.binding.entryIcon.let { pluginIcon ->
GlideApp.with(pluginIcon).clear(pluginIcon)
}
} }
super.onViewRecycled(holder) super.onViewRecycled(holder)
} }
@ -112,7 +117,7 @@ class PluginAdapter(
fun prettyCount(number: Number): String? { fun prettyCount(number: Number): String? {
val suffix = charArrayOf(' ', 'k', 'M', 'B', 'T', 'P', 'E') val suffix = charArrayOf(' ', 'k', 'M', 'B', 'T', 'P', 'E')
val numValue = number.toLong() val numValue = number.toLong()
val value = Math.floor(Math.log10(numValue.toDouble())).toInt() val value = floor(log10(numValue.toDouble())).toInt()
val base = value / 3 val base = value / 3
return if (value >= 3 && base < suffix.size) { return if (value >= 3 && base < suffix.size) {
DecimalFormat("#0.00").format( DecimalFormat("#0.00").format(
@ -127,8 +132,8 @@ class PluginAdapter(
} }
} }
inner class PluginViewHolder(itemView: View) : inner class PluginViewHolder(val binding: RepositoryItemBinding) :
RecyclerView.ViewHolder(itemView) { RecyclerView.ViewHolder(binding.root) {
fun bind( fun bind(
data: PluginViewData, data: PluginViewData,
@ -138,17 +143,17 @@ class PluginAdapter(
val name = metadata.name.removeSuffix("Provider") val name = metadata.name.removeSuffix("Provider")
val alpha = if (disabled) 0.6f else 1f val alpha = if (disabled) 0.6f else 1f
val isLocal = !data.plugin.second.url.startsWith("http") val isLocal = !data.plugin.second.url.startsWith("http")
itemView.main_text?.alpha = alpha binding.mainText.alpha = alpha
itemView.sub_text?.alpha = alpha binding.subText.alpha = alpha
val drawableInt = if (data.isDownloaded) val drawableInt = if (data.isDownloaded)
R.drawable.ic_baseline_delete_outline_24 R.drawable.ic_baseline_delete_outline_24
else R.drawable.netflix_download else R.drawable.netflix_download
itemView.nsfw_marker?.isVisible = metadata.tvTypes?.contains("NSFW") ?: false binding.nsfwMarker.isVisible = metadata.tvTypes?.contains("NSFW") ?: false
itemView.action_button?.setImageResource(drawableInt) binding.actionButton.setImageResource(drawableInt)
itemView.action_button?.setOnClickListener { binding.actionButton.setOnClickListener {
iconClickCallback.invoke(data.plugin) iconClickCallback.invoke(data.plugin)
} }
itemView.setOnClickListener { itemView.setOnClickListener {
@ -169,10 +174,11 @@ class PluginAdapter(
if (data.isDownloaded) { if (data.isDownloaded) {
// On local plugins page the filepath is provided instead of url. // 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) { if (plugin?.openSettings != null) {
itemView.action_settings?.isVisible = true binding.actionSettings.isVisible = true
itemView.action_settings.setOnClickListener { binding.actionSettings.setOnClickListener {
try { try {
plugin.openSettings!!.invoke(itemView.context) plugin.openSettings!!.invoke(itemView.context)
} catch (e: Throwable) { } catch (e: Throwable) {
@ -185,13 +191,13 @@ class PluginAdapter(
} }
} }
} else { } else {
itemView.action_settings?.isVisible = false binding.actionSettings.isVisible = false
} }
} else { } 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( metadata.iconUrl?.replace(
"%size%", "%size%",
"$iconSize" "$iconSize"
@ -201,41 +207,47 @@ class PluginAdapter(
), ),
null, null,
errorImageDrawable = R.drawable.ic_baseline_extension_24 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 binding.extVersion.isVisible = true
itemView.ext_version?.text = "v${metadata.version}" binding.extVersion.text = "v${metadata.version}"
if (metadata.language.isNullOrBlank()) { if (metadata.language.isNullOrBlank()) {
itemView.lang_icon?.isVisible = false binding.langIcon.isVisible = false
} else { } else {
itemView.lang_icon?.isVisible = true binding.langIcon.isVisible = true
itemView.lang_icon.text = "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" binding.langIcon.text =
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
} }
itemView.ext_votes?.isVisible = false binding.extVotes.isVisible = false
if (!isLocal) { if (!isLocal) {
ioSafe { ioSafe {
metadata.getVotes().main { metadata.getVotes().main {
itemView.ext_votes?.setText(txt(R.string.extension_rating, prettyCount(it))) binding.extVotes.setText(txt(R.string.extension_rating, prettyCount(it)))
itemView.ext_votes?.isVisible = true binding.extVotes.isVisible = true
} }
} }
} }
if (metadata.fileSize != null) { if (metadata.fileSize != null) {
itemView.ext_filesize?.isVisible = true binding.extFilesize.isVisible = true
itemView.ext_filesize?.text = formatShortFileSize(itemView.context, metadata.fileSize) binding.extFilesize.text = formatShortFileSize(itemView.context, metadata.fileSize)
} else { } 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)) binding.mainText.setText(
itemView.sub_text?.isGone = metadata.description.isNullOrBlank() if (disabled) txt(
itemView.sub_text?.text = metadata.description.html() 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.content.res.ColorStateList
import android.os.Bundle 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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 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
import com.lagradost.cloudstream3.plugins.VotingApi.canVote
import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.plugins.VotingApi.vote import com.lagradost.cloudstream3.plugins.VotingApi.vote
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main 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.fromTwoLettersToLanguage
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso 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() { 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( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.fragment_plugin_details, container, false) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val metadata = data.plugin.second val metadata = data.plugin.second
if (plugin_icon?.setImage(//plugin_icon?.height ?: binding?.apply {
if (!pluginIcon.setImage(//plugin_icon?.height ?:
metadata.iconUrl?.replace( metadata.iconUrl?.replace(
"%size%", "%size%",
"$iconSize" "$iconSize"
@ -64,23 +72,33 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
), ),
null, null,
errorImageDrawable = R.drawable.ic_baseline_extension_24 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") pluginName.text = metadata.name.removeSuffix("Provider")
plugin_version?.text = metadata.version.toString() pluginVersion.text = metadata.version.toString()
plugin_description?.text = metadata.description ?: getString(R.string.no_data) pluginDescription.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) pluginSize.text =
plugin_author?.text = if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(", ") if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(
plugin_status?.text = resources.getStringArray(R.array.extension_statuses)[metadata.status] context,
plugin_types?.text = if ((metadata.tvTypes == null) || metadata.tvTypes.isEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(", ") metadata.fileSize
plugin_lang?.text = if (metadata.language == null) )
getString(R.string.no_data) 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 else
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}" "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
github_btn.setOnClickListener { githubBtn.setOnClickListener {
if (metadata.repositoryUrl != null) { if (metadata.repositoryUrl != null) {
openBrowser(metadata.repositoryUrl) openBrowser(metadata.repositoryUrl)
} }
@ -93,10 +111,11 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
if (data.isDownloaded) { if (data.isDownloaded) {
// On local plugins page the filepath is provided instead of url. // 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) { if (plugin?.openSettings != null && context != null) {
action_settings?.isVisible = true actionSettings.isVisible = true
action_settings.setOnClickListener { actionSettings.setOnClickListener {
try { try {
plugin.openSettings!!.invoke(requireContext()) plugin.openSettings!!.invoke(requireContext())
} catch (e: Throwable) { } catch (e: Throwable) {
@ -109,10 +128,10 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
} }
} }
} else { } else {
action_settings?.isVisible = false actionSettings.isVisible = false
} }
} else { } else {
action_settings?.isVisible = false actionSettings.isVisible = false
} }
upvote.setOnClickListener { upvote.setOnClickListener {
@ -136,23 +155,40 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
updateVoting(it) updateVoting(it)
} }
} }
}
} }
private fun updateVoting(value: Int) { private fun updateVoting(value: Int) {
val metadata = data.plugin.second val metadata = data.plugin.second
plugin_votes.text = value.toString() binding?.apply {
when (metadata.getVoteType()) { pluginVotes.text = value.toString()
VotingApi.VoteType.UPVOTE -> { when (metadata.getVoteType()) {
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary) VotingApi.VoteType.UPVOTE -> {
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) upvote.imageTintList = ColorStateList.valueOf(
} context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary
VotingApi.VoteType.DOWNVOTE -> { )
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary) downvote.imageTintList = ColorStateList.valueOf(
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white) 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) 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.AllLanguagesName
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.toPx 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_NAME = "name"
const val PLUGINS_BUNDLE_URL = "url" const val PLUGINS_BUNDLE_URL = "url"
@ -33,11 +31,19 @@ class PluginsFragment : Fragment() {
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
): View? { ): View {
return inflater.inflate(R.layout.fragment_plugins, container, false) 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() private val pluginViewModel: PluginsViewModel by activityViewModels()
var binding: FragmentPluginsBinding? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -66,8 +72,8 @@ class PluginsFragment : Fragment() {
} }
setUpToolbar(name) setUpToolbar(name)
binding?.settingsToolbar?.apply {
settings_toolbar?.setOnMenuItemClickListener { menuItem -> setOnMenuItemClickListener { menuItem ->
when (menuItem?.itemId) { when (menuItem?.itemId) {
R.id.download_all -> { R.id.download_all -> {
PluginsViewModel.downloadAll(activity, url, pluginViewModel) PluginsViewModel.downloadAll(activity, url, pluginViewModel)
@ -99,67 +105,69 @@ class PluginsFragment : Fragment() {
} }
val searchView = 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 // Don't go back if active query
settings_toolbar?.setNavigationOnClickListener { setNavigationOnClickListener {
if (searchView?.isIconified == false) { if (searchView?.isIconified == false) {
searchView.isIconified = true searchView.isIconified = true
} else { } else {
activity?.onBackPressed() 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 = { // searchView?.onActionViewCollapsed = {
// pluginViewModel.search(null) // pluginViewModel.search(null)
// } // }
// Because onActionViewCollapsed doesn't wanna work we need this workaround :( // 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 { PluginAdapter {
pluginViewModel.handlePluginAction(activity, url, it, isLocal) pluginViewModel.handlePluginAction(activity, url, it, isLocal)
} }
if (isTvSettings()) { if (isTvSettings()) {
// Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that. // 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) -> observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
(plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list) (binding?.pluginRecyclerView?.adapter as? PluginAdapter)?.updateList(list)
if (scrollToTop) if (scrollToTop)
plugin_recycler_view?.scrollToPosition(0) binding?.pluginRecyclerView?.scrollToPosition(0)
} }
if (isLocal) { if (isLocal) {
// No download button and no categories on local // No download button and no categories on local
settings_toolbar?.menu?.findItem(R.id.download_all)?.isVisible = false binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false
settings_toolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false
pluginViewModel.updatePluginListLocal() pluginViewModel.updatePluginListLocal()
tv_types_scroll_view?.isVisible = false
binding?.tvtypesChipsScroll?.root?.isVisible = false
} else { } else {
pluginViewModel.updatePluginList(context, url) 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.clear()
pluginViewModel.tvTypes.addAll(list.map { it.name }) pluginViewModel.tvTypes.addAll(list.map { it.name })
pluginViewModel.updateFilteredPlugins() pluginViewModel.updateFilteredPlugins()

View file

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

View file

@ -1,14 +1,15 @@
package com.lagradost.cloudstream3.ui.settings.extensions package com.lagradost.cloudstream3.ui.settings.extensions
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R 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.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import kotlinx.android.synthetic.main.repository_item.view.*
class RepoAdapter( class RepoAdapter(
val isSetup: Boolean, val isSetup: Boolean,
@ -20,9 +21,17 @@ class RepoAdapter(
private val repositories: MutableList<RepositoryData> = mutableListOf() private val repositories: MutableList<RepositoryData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 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( return RepoViewHolder(
LayoutInflater.from(parent.context).inflate(layout, parent, false) layout
) )
} }
@ -57,30 +66,57 @@ class RepoAdapter(
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
} }
inner class RepoViewHolder(itemView: View) : inner class RepoViewHolder(
RecyclerView.ViewHolder(itemView) { val binding: ViewBinding
) :
RecyclerView.ViewHolder(binding.root) {
fun bind( fun bind(
repositoryData: RepositoryData repositoryData: RepositoryData
) { ) {
val isPrebuilt = PREBUILT_REPOSITORIES.contains(repositoryData) val isPrebuilt = PREBUILT_REPOSITORIES.contains(repositoryData)
val drawable = val drawable =
if (isSetup) R.drawable.netflix_download else R.drawable.ic_baseline_delete_outline_24 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. actionButton.setOnClickListener {
// No delete buttons on prebuilt repos. imageClickCallback(repositoryData)
if (!isPrebuilt || isSetup) { }
itemView.action_button?.setImageResource(drawable)
}
itemView.action_button?.setOnClickListener { repositoryItemRoot.setOnClickListener {
imageClickCallback(repositoryData) clickCallback(repositoryData)
} }
mainText.text = repositoryData.name
subText.text = repositoryData.url
}
}
itemView.repository_item_root?.setOnClickListener { is RepositoryItemBinding -> {
clickCallback(repositoryData) 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 package com.lagradost.cloudstream3.ui.settings.testing
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentTestingBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable import com.lagradost.cloudstream3.mvvm.observeNullable
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar 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() { class TestFragment : Fragment() {
private val testViewModel: TestViewModel by activityViewModels() private val testViewModel: TestViewModel by activityViewModels()
var binding: FragmentTestingBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
setUpToolbar(R.string.category_provider_test) setUpToolbar(R.string.category_provider_test)
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
provider_test_recycler_view?.adapter = TestResultAdapter( binding?.apply {
mutableListOf() providerTestRecyclerView.adapter = TestResultAdapter(
) mutableListOf()
)
testViewModel.init() testViewModel.init()
if (testViewModel.isRunningTest) { if (testViewModel.isRunningTest) {
provider_test?.setState(TestView.TestState.Running) providerTest.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
)
} }
}
provider_test?.setOnPlayButtonListener { state -> observe(testViewModel.providerProgress) { (passed, failed, total) ->
when (state) { providerTest.setProgress(passed, failed, total)
TestView.TestState.Stopped -> testViewModel.stopTest()
TestView.TestState.Running -> testViewModel.startTest()
TestView.TestState.None -> testViewModel.startTest()
} }
}
if (isTrueTvSettings()) { observeNullable(testViewModel.providerResults) {
tests_play_pause?.isFocusableInTouchMode = true normalSafeApiCall {
tests_play_pause?.requestFocus() val newItems = it.sortedBy { api -> api.first.name }
} (providerTestRecyclerView.adapter as? TestResultAdapter)?.updateList(
newItems
provider_test?.playPauseButton?.setOnFocusChangeListener { _, hasFocus -> )
if (hasFocus) { }
provider_test_appbar?.setExpanded(true, true) }
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()) { if (isTrueTvSettings()) {
provider_test_recycler_view?.requestFocus() providerTest.playPauseButton?.isFocusableInTouchMode = true
provider_test_appbar?.setExpanded(false, true) providerTest.playPauseButton?.requestFocus()
} }
}
provider_test?.setOnMainClick { providerTest.playPauseButton?.setOnFocusChangeListener { _, hasFocus ->
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All) if (hasFocus) {
focusRecyclerView() providerTestAppbar.setExpanded(true, true)
} }
provider_test?.setOnFailedClick { }
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed)
focusRecyclerView() fun focusRecyclerView() {
} // Hack to make it possible to focus the recyclerview.
provider_test?.setOnPassedClick { if (isTrueTvSettings()) {
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed) providerTestRecyclerView.requestFocus()
focusRecyclerView() 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( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.fragment_testing, container, false) 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 androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.MainAPI import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ProviderTestItemBinding
import com.lagradost.cloudstream3.mvvm.getAllMessages import com.lagradost.cloudstream3.mvvm.getAllMessages
import com.lagradost.cloudstream3.mvvm.getStackTracePretty import com.lagradost.cloudstream3.mvvm.getStackTracePretty
import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
import com.lagradost.cloudstream3.utils.TestingUtils 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>>) : class TestResultAdapter(override val items: MutableList<Pair<MainAPI, TestingUtils.TestResultProvider>>) :
AppUtils.DiffAdapter<Pair<MainAPI, TestingUtils.TestResultProvider>>(items) { AppUtils.DiffAdapter<Pair<MainAPI, TestingUtils.TestResultProvider>>(items) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ProviderTestViewHolder( return ProviderTestViewHolder(
LayoutInflater.from(parent.context) ProviderTestItemBinding.inflate(LayoutInflater.from(parent.context), parent,false)
.inflate(R.layout.provider_test_item, 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) { inner class ProviderTestViewHolder(binding: ProviderTestItemBinding) : RecyclerView.ViewHolder(binding.root) {
private val languageText: TextView = itemView.lang_icon private val languageText: TextView = binding.langIcon
private val providerTitle: TextView = itemView.main_text private val providerTitle: TextView = binding.mainText
private val statusText: TextView = itemView.passed_failed_marker private val statusText: TextView = binding.passedFailedMarker
private val failDescription: TextView = itemView.fail_description private val failDescription: TextView = binding.failDescription
private val logButton: ImageView = itemView.action_button private val logButton: ImageView = binding.actionButton
private fun String.lastLine(): String? { private fun String.lastLine(): String? {
return this.lines().lastOrNull { it.isNotBlank() } 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import okhttp3.internal.toImmutableList
class TestViewModel : ViewModel() { class TestViewModel : ViewModel() {
data class TestProgress( data class TestProgress(
@ -81,15 +82,14 @@ class TestViewModel : ViewModel() {
} }
fun init() { fun init() {
val apis = APIHolder.allProviders total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
total = apis.size
updateProgress() updateProgress()
} }
fun startTest() { fun startTest() {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
val apis = APIHolder.allProviders val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
total = apis.size total = apis.size
failed = 0 failed = 0
passed = 0 passed = 0

View file

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

View file

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

View file

@ -10,30 +10,39 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.R 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 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 import org.acra.ACRA
class SetupFragmentLayout : Fragment() { class SetupFragmentLayout : Fragment() {
var binding: FragmentSetupLayoutBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.fragment_setup_layout, container, false) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root) fixPaddingStatusbar(binding?.setupRoot)
with(context) { normalSafeApiCall {
if (this == null) return val ctx = context ?: return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val prefNames = resources.getStringArray(R.array.app_layout) val prefNames = resources.getStringArray(R.array.app_layout)
val prefValues = resources.getIntArray(R.array.app_layout_values) 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) settingsManager.getInt(getString(R.string.app_layout_key), -1)
val arrayAdapter = val arrayAdapter =
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice) ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
arrayAdapter.addAll(prefNames.toList()) arrayAdapter.addAll(prefNames.toList())
listview1?.adapter = arrayAdapter binding?.apply {
listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE listview1.adapter = arrayAdapter
listview1?.setItemChecked( listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE
prefValues.indexOf(currentLayout), true 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
) )
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 { val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true)
findNavController().navigate(R.id.navigation_home)
}
prev_btt?.setOnClickListener { acraSwitch.isChecked = enableCrashReporting
findNavController().popBackStack() 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.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar 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() { class SetupFragmentMedia : Fragment() {
var binding: FragmentSetupMediaBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.fragment_setup_media, container, false) 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root) normalSafeApiCall {
fixPaddingStatusbar(binding?.setupRoot)
with(context) { val ctx = context ?: return@normalSafeApiCall
if (this == null) return val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val arrayAdapter = 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 names = enumValues<TvType>().sorted().map { it.name }
val selected = mutableListOf<Int>() val selected = mutableListOf<Int>()
arrayAdapter.addAll(names) arrayAdapter.addAll(names)
listview1?.let { binding?.apply {
it.adapter = arrayAdapter listview1.let {
it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE it.adapter = arrayAdapter
it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
it.setOnItemClickListener { _, _, _, _ -> it.setOnItemClickListener { _, _, _, _ ->
it.checkedItemPositions?.forEach { key, value -> it.checkedItemPositions?.forEach { key, value ->
if (value) { if (value) {
selected.add(key) selected.add(key)
} else { } else {
selected.remove(key) 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 { nextBtt.setOnClickListener {
findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout) findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout)
} }
prev_btt?.setOnClickListener { prevBtt.setOnClickListener {
findNavController().popBackStack() findNavController().popBackStack()
}
} }
} }
} }
} }

View file

@ -14,33 +14,45 @@ import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.R 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.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import kotlinx.android.synthetic.main.fragment_setup_media.*
class SetupFragmentProviderLanguage : Fragment() { class SetupFragmentProviderLanguage : Fragment() {
var binding: FragmentSetupProviderLanguagesBinding? = null
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View {
// Inflate the layout for this fragment val localBinding = FragmentSetupProviderLanguagesBinding.inflate(inflater, container, false)
return inflater.inflate(R.layout.fragment_setup_provider_languages, 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?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.fixPaddingStatusbar(setup_root) fixPaddingStatusbar(binding?.setupRoot)
with(context) { normalSafeApiCall {
if (this == null) return val ctx = context ?: return@normalSafeApiCall
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
val arrayAdapter = 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 current = ctx.getApiProviderLangSettings()
val langs = APIHolder.apis.map { it.lang }.toSet() val langs = synchronized(APIHolder.apis) { APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName}
val currentList = val currentList =
current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO
@ -56,31 +68,31 @@ class SetupFragmentProviderLanguage : Fragment() {
} }
arrayAdapter.addAll(languageNames) arrayAdapter.addAll(languageNames)
binding?.apply {
listview1?.adapter = arrayAdapter listview1.adapter = arrayAdapter
listview1?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE listview1.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
currentList.forEach { currentList.forEach {
listview1.setItemChecked(it, true) listview1.setItemChecked(it, true)
} }
listview1?.setOnItemClickListener { _, _, _, _ -> listview1.setOnItemClickListener { _, _, _, _ ->
val currentLanguages = mutableListOf<String>() val currentLanguages = mutableListOf<String>()
listview1?.checkedItemPositions?.forEach { key, value -> listview1.checkedItemPositions?.forEach { key, value ->
if (value) currentLanguages.add(langs[key]) if (value) currentLanguages.add(langs[key])
} }
settingsManager.edit().putStringSet( settingsManager.edit().putStringSet(
this.getString(R.string.provider_lang_key), ctx.getString(R.string.provider_lang_key),
currentLanguages.toSet() currentLanguages.toSet()
).apply() ).apply()
} }
next_btt?.setOnClickListener { nextBtt.setOnClickListener {
findNavController().navigate(R.id.navigation_setup_provider_languages_to_navigation_setup_media) findNavController().navigate(R.id.navigation_setup_provider_languages_to_navigation_setup_media)
} }
prev_btt?.setOnClickListener { prevBtt.setOnClickListener {
findNavController().popBackStack() 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.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event 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.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.subtitle_settings.*
const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings" const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings"
@ -137,12 +137,21 @@ class ChromecastSubtitlesFragment : Fragment() {
//subtitle_text?.setStyle(fromSaveToStyle(state)) //subtitle_text?.setStyle(fromSaveToStyle(state))
} }
var binding : ChromecastSubtitleSettingsBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
): View? { ): View {
return inflater.inflate(R.layout.chromecast_subtitle_settings, container, false) 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 private lateinit var state: SaveChromeCaptionStyle
@ -159,7 +168,7 @@ class ChromecastSubtitlesFragment : Fragment() {
onColorSelectedEvent += ::onColorSelected onColorSelectedEvent += ::onColorSelected
onDialogDismissedEvent += ::onDialogDismissed onDialogDismissedEvent += ::onDialogDismissed
context?.fixPaddingStatusbar(subs_root) fixPaddingStatusbar(binding?.subsRoot)
state = getCurrentSavedStyle() state = getCurrentSavedStyle()
context?.updateState() context?.updateState()
@ -185,22 +194,25 @@ class ChromecastSubtitlesFragment : Fragment() {
this.setOnLongClickListener { this.setOnLongClickListener {
it.context.setColor(id, null) 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 return@setOnLongClickListener true
} }
} }
subs_text_color.setup(0) binding?.apply {
subs_outline_color.setup(1) subsTextColor.setup(0)
subs_background_color.setup(2) subsOutlineColor.setup(1)
subsBackgroundColor.setup(2)
}
val dismissCallback = { val dismissCallback = {
if (hide) if (hide)
activity?.hideSystemUI() activity?.hideSystemUI()
} }
subs_edge_type.setFocusableInTv() binding?.subsEdgeType?.setFocusableInTv()
subs_edge_type.setOnClickListener { textView -> binding?.subsEdgeType?.setOnClickListener { textView ->
val edgeTypes = listOf( val edgeTypes = listOf(
Pair( Pair(
EDGE_TYPE_NONE, EDGE_TYPE_NONE,
@ -237,15 +249,15 @@ class ChromecastSubtitlesFragment : Fragment() {
} }
} }
subs_edge_type.setOnLongClickListener { binding?.subsEdgeType?.setOnLongClickListener {
state.edgeType = defaultState.edgeType state.edgeType = defaultState.edgeType
it.context.updateState() 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 return@setOnLongClickListener true
} }
subs_font_size.setFocusableInTv() binding?.subsFontSize?.setFocusableInTv()
subs_font_size.setOnClickListener { textView -> binding?.subsFontSize?.setOnClickListener { textView ->
val fontSizes = listOf( val fontSizes = listOf(
Pair(0.75f, "75%"), Pair(0.75f, "75%"),
Pair(0.80f, "80%"), Pair(0.80f, "80%"),
@ -278,24 +290,26 @@ class ChromecastSubtitlesFragment : Fragment() {
} }
} }
subs_font_size.setOnLongClickListener { _ -> binding?.subsFontSize?.setOnLongClickListener { _ ->
state.fontScale = defaultState.fontScale state.fontScale = defaultState.fontScale
//textView.context.updateState() // font size not changed //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 return@setOnLongClickListener true
} }
subs_font.setFocusableInTv()
subs_font.setOnClickListener { textView ->
binding?.subsFont?.setFocusableInTv()
binding?.subsFont?.setOnClickListener { textView ->
val fontTypes = listOf( val fontTypes = listOf(
Pair(null, textView.context.getString(R.string.normal)), null to textView.context.getString(R.string.normal),
Pair("Droid Sans", "Droid Sans"), "Droid Sans" to "Droid Sans",
Pair("Droid Sans Mono", "Droid Sans Mono"), "Droid Sans Mono" to "Droid Sans Mono",
Pair("Droid Serif Regular", "Droid Serif Regular"), "Droid Serif Regular" to "Droid Serif Regular",
Pair("Cutive Mono", "Cutive Mono"), "Cutive Mono" to "Cutive Mono",
Pair("Short Stack", "Short Stack"), "Short Stack" to "Short Stack",
Pair("Quintessential", "Quintessential"), "Quintessential" to "Quintessential",
Pair("Alegreya Sans SC", "Alegreya Sans SC"), "Alegreya Sans SC" to "Alegreya Sans SC",
) )
//showBottomDialog //showBottomDialog
@ -310,35 +324,35 @@ class ChromecastSubtitlesFragment : Fragment() {
textView.context.updateState() textView.context.updateState()
} }
} }
binding?.subsFont?.setOnLongClickListener { textView ->
subs_font.setOnLongClickListener { textView ->
state.fontFamily = defaultState.fontFamily state.fontFamily = defaultState.fontFamily
textView.context.updateState() textView.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true return@setOnLongClickListener true
} }
cancel_btt.setOnClickListener { binding?.cancelBtt?.setOnClickListener {
activity?.popCurrentPage() activity?.popCurrentPage()
} }
apply_btt.setOnClickListener { binding?.applyBtt?.setOnClickListener {
it.context.saveStyle(state) it.context.saveStyle(state)
applyStyleEvent.invoke(state) applyStyleEvent.invoke(state)
//it.context.fromSaveToStyle(state) //it.context.fromSaveToStyle(state)
activity?.popCurrentPage() activity?.popCurrentPage()
} }
binding?.subtitleText?.apply {
subtitle_text.setCues( setCues(
listOf( listOf(
Cue.Builder() Cue.Builder()
.setTextSize( .setTextSize(
getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(), getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(),
Cue.TEXT_SIZE_TYPE_ABSOLUTE Cue.TEXT_SIZE_TYPE_ABSOLUTE
) )
.setText(subtitle_text.context.getString(R.string.subtitles_example_text)) .setText(context.getString(R.string.subtitles_example_text))
.build() .build()
)
) )
) }
} }
} }

View file

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event 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.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage 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 import java.io.File
const val SUBTITLE_KEY = "subtitle_settings" const val SUBTITLE_KEY = "subtitle_settings"
@ -184,10 +183,10 @@ class SubtitlesFragment : Fragment() {
} }
private fun Context.updateState() { private fun Context.updateState() {
subtitle_text?.setStyle(fromSaveToStyle(state)) binding?.subtitleText?.setStyle(fromSaveToStyle(state))
val text = subtitle_text.context.getString(R.string.subtitles_example_text) val text = getString(R.string.subtitles_example_text)
val fixedText = if (state.upperCase) text.uppercase() else text val fixedText = if (state.upperCase) text.uppercase() else text
subtitle_text?.setCues( binding?.subtitleText?.setCues(
listOf( listOf(
Cue.Builder() Cue.Builder()
.setTextSize( .setTextSize(
@ -213,12 +212,21 @@ class SubtitlesFragment : Fragment() {
return if (color == Color.TRANSPARENT) Color.BLACK else color return if (color == Color.TRANSPARENT) Color.BLACK else color
} }
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
var binding: SubtitleSettingsBinding? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle?, savedInstanceState: Bundle?
): View? { ): View {
return inflater.inflate(R.layout.subtitle_settings, container, false) 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 private lateinit var state: SaveCaptionStyle
@ -234,11 +242,11 @@ class SubtitlesFragment : Fragment() {
hide = arguments?.getBoolean("hide") ?: true hide = arguments?.getBoolean("hide") ?: true
onColorSelectedEvent += ::onColorSelected onColorSelectedEvent += ::onColorSelected
onDialogDismissedEvent += ::onDialogDismissed 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?.getExternalFilesDir(null)?.absolutePath.toString() + "/Fonts"
) )
context?.fixPaddingStatusbar(subs_root) fixPaddingStatusbar(binding?.subsRoot)
state = getCurrentSavedStyle() state = getCurrentSavedStyle()
context?.updateState() context?.updateState()
@ -264,317 +272,318 @@ class SubtitlesFragment : Fragment() {
this.setOnLongClickListener { this.setOnLongClickListener {
it.context.setColor(id, null) 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 return@setOnLongClickListener true
} }
} }
binding?.apply {
subsTextColor.setup(0)
subsOutlineColor.setup(1)
subsBackgroundColor.setup(2)
subsWindowColor.setup(3)
subs_text_color.setup(0) val dismissCallback = {
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()
if (hide) if (hide)
activity?.hideSystemUI() activity?.hideSystemUI()
} }
}
subs_subtitle_elevation.setOnLongClickListener { subsSubtitleElevation.setFocusableInTv()
state.elevation = DEF_SUBS_ELEVATION subsSubtitleElevation.setOnClickListener { textView ->
it.context.updateState() val suffix = "dp"
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) val elevationTypes = listOf(
return@setOnLongClickListener true 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() //showBottomDialog
subs_edge_type.setOnClickListener { textView -> activity?.showDialog(
val edgeTypes = listOf( elevationTypes.map { it.second },
Pair( elevationTypes.map { it.first }.indexOf(state.elevation),
CaptionStyleCompat.EDGE_TYPE_NONE, (textView as TextView).text.toString(),
textView.context.getString(R.string.subtitles_none) false,
), dismissCallback
Pair( ) { index ->
CaptionStyleCompat.EDGE_TYPE_OUTLINE, state.elevation = elevationTypes.map { it.first }[index]
textView.context.getString(R.string.subtitles_outline) textView.context.updateState()
), if (hide)
Pair( activity?.hideSystemUI()
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
} }
}
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() textView.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
} }
}
subs_font.setOnLongClickListener { textView -> subsAutoSelectLanguage.setFocusableInTv()
state.typeface = null subsAutoSelectLanguage.setOnClickListener { textView ->
state.typefaceFilePath = null val langMap = arrayListOf(
textView.context.updateState() SubtitleHelper.Language639(
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) textView.context.getString(R.string.none),
return@setOnLongClickListener true textView.context.getString(R.string.none),
} "",
"",
"",
"",
""
),
)
langMap.addAll(SubtitleHelper.languages)
subs_auto_select_language.setFocusableInTv() val lang639_1 = langMap.map { it.ISO_639_1 }
subs_auto_select_language.setOnClickListener { textView -> activity?.showDialog(
val langMap = arrayListOf( langMap.map { it.languageName },
SubtitleHelper.Language639( lang639_1.indexOf(getAutoSelectLanguageISO639_1()),
textView.context.getString(R.string.none), (textView as TextView).text.toString(),
textView.context.getString(R.string.none), true,
"", dismissCallback
"", ) { index ->
"", setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
"", }
""
),
)
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])
} }
}
subs_auto_select_language.setOnLongClickListener { subsAutoSelectLanguage.setOnLongClickListener {
setKey(SUBTITLE_AUTO_SELECT_KEY, "en") setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT) showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true 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())
} }
}
subs_download_languages.setOnLongClickListener { subsDownloadLanguages.setFocusableInTv()
setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en")) 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) activity?.showMultiDialog(
return@setOnLongClickListener true 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 { subsDownloadLanguages.setOnLongClickListener {
activity?.popCurrentPage() setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
}
apply_btt.setOnClickListener { showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
it.context.saveStyle(state) return@setOnLongClickListener true
applyStyleEvent.invoke(state) }
it.context.fromSaveToStyle(state)
activity?.popCurrentPage() 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.provider.MediaStore
import android.text.Spanned import android.text.Spanned
import android.util.Log 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.view.animation.DecelerateInterpolator
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@ -33,6 +36,7 @@ import androidx.core.widget.ContentLoadingProgressBar
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView 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.gms.common.wrappers.Wrappers
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching 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.WebviewFragment
import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings 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 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() { fun BottomSheetDialog?.ownHide() {
this?.hide() this?.hide()
} }
@ -198,7 +207,11 @@ object AppUtils {
animation.start() 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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_DEFAULT val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = 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 // https://github.com/googlearchive/leanback-homescreen-channels/blob/master/app/src/main/java/com/google/android/tvhomescreenchannels/SampleTvProvider.java
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@Throws
@WorkerThread @WorkerThread
suspend fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) { suspend fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
@ -369,7 +383,6 @@ object AppUtils {
) )
main { main {
showToast( showToast(
this@loadRepository,
getString(R.string.player_loaded_subtitles, repo.name), getString(R.string.player_loaded_subtitles, repo.name),
Toast.LENGTH_LONG 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( fun FragmentActivity.loadResult(
url: String, url: String,
apiName: String, apiName: String,
startAction: Int = 0, startAction: Int = 0,
startValue: 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 { this.runOnUiThread {
// viewModelStore.clear() // viewModelStore.clear()
this.navigate( 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( fun Activity?.loadSearchResult(
card: SearchResponse, card: SearchResponse,
startAction: Int = 0, startAction: Int = 0,
@ -776,12 +814,12 @@ object AppUtils {
return networkInfo.any { return networkInfo.any {
conManager.getNetworkCapabilities(it) conManager.getNetworkCapabilities(it)
?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true ?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
} && } &&
!networkInfo.any { !networkInfo.any {
conManager.getNetworkCapabilities(it) conManager.getNetworkCapabilities(it)
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
} }
} }
private fun Activity?.cacheClass(clazz: String?) { private fun Activity?.cacheClass(clazz: String?) {

View file

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

View file

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

View file

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

View file

@ -399,6 +399,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Moviehab(), Moviehab(),
MoviehabNet(), MoviehabNet(),
Jeniusplay(), Jeniusplay(),
StreamoUpload(),
Gdriveplayerapi(), Gdriveplayerapi(),
Gdriveplayerapp(), Gdriveplayerapp(),
@ -548,4 +549,4 @@ abstract class ExtractorApi {
open fun getExtractorUrl(id: String): String { open fun getExtractorUrl(id: String): String {
return id return id
} }
} }

View file

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

View file

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

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