Merge pull request #507 from recloudstream/viewbindings

Migration to viewbindings
This commit is contained in:
Osten 2023-07-30 15:15:44 +02:00 committed by GitHub
commit 4b4e006f4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
168 changed files with 9653 additions and 6681 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import android.content.res.Resources
import android.os.Build
import android.util.Log
import android.view.*
import android.view.View.NO_ID
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
@ -27,6 +28,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper
@ -34,9 +36,18 @@ import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import org.schabi.newpipe.extractor.NewPipe
import java.lang.ref.WeakReference
import java.util.*
object CommonActivity {
private var _activity: WeakReference<Activity>? = null
var activity
get() = _activity?.get()
private set(value) {
_activity = WeakReference(value)
}
@MainThread
fun Activity?.getCastSession(): CastSession? {
return (this as MainActivity?)?.mSessionManager?.currentCastSession
@ -56,6 +67,30 @@ object CommonActivity {
var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return
act.runOnUiThread {
showToast(act, act.getString(message), duration)
}
}
fun showToast(message: String?, duration: Int? = null) {
val act = activity ?: return
act.runOnUiThread {
showToast(act, message, duration)
}
}
fun showToast(message: UiText?, duration: Int? = null) {
val act = activity ?: return
if (message == null) return
act.runOnUiThread {
showToast(act, message.asString(act), duration)
}
}
@MainThread
fun showToast(act: Activity?, text: UiText, duration: Int) {
if (act == null) return
text.asStringNull(act)?.let {
@ -140,6 +175,7 @@ object CommonActivity {
fun init(act: ComponentActivity?) {
if (act == null) return
activity = act
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
//https://developer.android.com/guide/topics/ui/picture-in-picture
canShowPipMode =
@ -222,6 +258,7 @@ object CommonActivity {
"AmoledLight" -> R.style.AmoledModeLight
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.MonetMode else R.style.AppTheme
else -> R.style.AppTheme
}
@ -244,8 +281,10 @@ object CommonActivity {
"Pink" -> R.style.OverlayPrimaryColorPink
"Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonet else R.style.OverlayPrimaryColorNormal
"Monet2" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
R.style.OverlayPrimaryColorMonetTwo else R.style.OverlayPrimaryColorNormal
else -> R.style.OverlayPrimaryColorNormal
}
act.theme.applyStyle(currentTheme, true)
@ -257,52 +296,94 @@ object CommonActivity {
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
}
/** because we want closes find, aka when multiple have the same id, we go to parent
until the correct one is found */
private fun localLook(from: View, id: Int): View? {
if (id == NO_ID) return null
var currentLook: View = from
while (true) {
currentLook.findViewById<View?>(id)?.let { return it }
currentLook = (currentLook.parent as? View) ?: break
}
return null
}
/*var currentLook: View = view
while (true) {
val tmpNext = currentLook.findViewById<View?>(nextId)
if (tmpNext != null) {
next = tmpNext
break
}
currentLook = currentLook.parent as? View ?: break
}*/
/** recursively looks for a next focus up to a depth of 10,
* this is used to override the normal shit focus system
* because this application has a lot of invisible views that messes with some tv devices*/
private fun getNextFocus(
act: Activity?,
view: View?,
direction: FocusDirection,
depth: Int = 0
): Int? {
): View? {
// if input is invalid let android decide + depth test to not crash if loop is found
if (view == null || depth >= 10 || act == null) {
return null
}
val nextId = when (direction) {
FocusDirection.Left -> {
var nextId = when (direction) {
FocusDirection.Start -> {
if (view.isRtl())
view.nextFocusRightId
else
view.nextFocusLeftId
}
FocusDirection.Up -> {
view.nextFocusUpId
}
FocusDirection.Right -> {
FocusDirection.End -> {
if (view.isRtl())
view.nextFocusLeftId
else
view.nextFocusRightId
}
FocusDirection.Down -> {
view.nextFocusDownId
}
}
return if (nextId != -1) {
val next = act.findViewById<View?>(nextId)
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
if (next?.isShown == false) {
getNextFocus(act, next, direction, depth + 1)
} else {
if (depth == 0) {
null
} else {
nextId
}
}
} else {
null
}
if (nextId == NO_ID) {
// if not specified then use forward id
nextId = view.nextFocusForwardId
// if view is still not found to next focus then return and let android decide
if (nextId == NO_ID) return null
}
enum class FocusDirection {
Left,
Right,
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
}
private enum class FocusDirection {
Start,
End,
Up,
Down,
}
@ -328,30 +409,39 @@ object CommonActivity {
KeyEvent.KEYCODE_FORWARD, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
PlayerEventType.SeekForward
}
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD, KeyEvent.KEYCODE_MEDIA_REWIND -> {
PlayerEventType.SeekBack
}
KeyEvent.KEYCODE_MEDIA_NEXT, KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_N -> {
PlayerEventType.NextEpisode
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS, KeyEvent.KEYCODE_BUTTON_L1, KeyEvent.KEYCODE_B -> {
PlayerEventType.PrevEpisode
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
PlayerEventType.Pause
}
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_BUTTON_START -> {
PlayerEventType.Play
}
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_7 -> {
PlayerEventType.Lock
}
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_MENU -> {
PlayerEventType.ToggleHide
}
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_VOLUME_MUTE -> {
PlayerEventType.ToggleMute
}
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_9 -> {
PlayerEventType.ShowMirrors
}
@ -359,21 +449,27 @@ object CommonActivity {
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_8 -> {
PlayerEventType.SearchSubtitlesOnline
}
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_3 -> {
PlayerEventType.ShowSpeed
}
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_0 -> {
PlayerEventType.Resize
}
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_4 -> {
PlayerEventType.SkipOp
}
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_5 -> {
PlayerEventType.SkipCurrentChapter
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_ENTER -> { // space is not captured due to navigation
PlayerEventType.PlayPauseToggle
}
else -> null
}?.let { playerEvent ->
playerEventListener?.invoke(playerEvent)
@ -386,64 +482,64 @@ object CommonActivity {
//}
}
/** overrides focus and custom key events */
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
if (act == null) return null
val currentFocus = act.currentFocus
event?.keyCode?.let { keyCode ->
when (event.action) {
KeyEvent.ACTION_DOWN -> {
if (act.currentFocus != null) {
val next = when (keyCode) {
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
val nextView = when (keyCode) {
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Left
currentFocus,
FocusDirection.Start
)
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
act,
act.currentFocus,
FocusDirection.Right
currentFocus,
FocusDirection.End
)
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
act,
act.currentFocus,
currentFocus,
FocusDirection.Up
)
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
act,
act.currentFocus,
currentFocus,
FocusDirection.Down
)
else -> null
}
if (next != null && next != -1) {
val nextView = act.findViewById<View?>(next)
if (nextView != null) {
nextView.requestFocus()
keyEventListener?.invoke(Pair(event, true))
return true
}
}
when (keyCode) {
KeyEvent.KEYCODE_DPAD_CENTER -> {
if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
) {
UIHelper.showInputMethod(act.currentFocus?.findFocus())
}
}
}
}
//println("Keycode: $keyCode")
//showToast(
// this,
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
// Toast.LENGTH_LONG
//)
}
}
}
// if someone else want to override the focus then don't handle the event as it is already
// consumed. used in video player
if (keyEventListener?.invoke(Pair(event, false)) == true) {
return true
}

View File

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

View File

@ -1,5 +1,7 @@
package com.lagradost.cloudstream3
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@ -14,9 +16,13 @@ import android.view.*
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.animation.addListener
import androidx.core.view.ViewCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
@ -32,6 +38,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.android.gms.cast.framework.*
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigationrail.NavigationRailView
import com.google.android.material.snackbar.Snackbar
@ -49,6 +56,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.CommonActivity.updateLocale
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.plugins.PluginManager
@ -74,6 +84,7 @@ import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
@ -86,12 +97,14 @@ import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.isLtr
import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.BackupUtils.backup
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
@ -108,17 +121,17 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.bottom_resultview_preview.*
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.lang.ref.WeakReference
import java.net.URI
import java.net.URLDecoder
import java.nio.charset.Charset
import kotlin.math.absoluteValue
import kotlin.reflect.KClass
import kotlin.system.exitProcess
@ -306,7 +319,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this@with.runOnUiThread {
try {
showToast(
this@with,
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
api.name
)
@ -334,8 +346,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
// Use both navigation views to support both layouts.
// It might be better to use the QuickSearch.
nav_view?.selectedItemId = R.id.navigation_search
nav_rail_view?.selectedItemId = R.id.navigation_search
activity?.findViewById<BottomNavigationView>(R.id.nav_view)?.selectedItemId =
R.id.navigation_search
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
R.id.navigation_search
} else if (safeURI(str)?.scheme == appStringPlayer) {
val uri = Uri.parse(str)
val name = uri.getQueryParameter("name")
@ -368,6 +382,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.navigate(R.id.navigation_downloads)
return true
} else {
synchronized(apis) {
for (api in apis) {
if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name)
@ -377,6 +392,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
}
}
return false
}
}
@ -412,7 +428,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.hideKeyboard()
// Fucks up anime info layout since that has its own layout
cast_mini_controller_holder?.isVisible =
binding?.castMiniControllerHolder?.isVisible =
!listOf(
R.id.navigation_results_phone,
R.id.navigation_results_tv,
@ -448,15 +464,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
R.id.navigation_player,
).contains(destination.id)
nav_host_fragment?.apply {
binding?.navHostFragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams
val push =
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
if (!this.isLtr()) {
params.setMargins(
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
params.leftMargin,
params.topMargin,
push,
params.bottomMargin
)
} else {
params.setMargins(
push,
params.topMargin,
params.rightMargin,
params.bottomMargin
)
}
layoutParams = params
}
@ -464,21 +492,24 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
Configuration.ORIENTATION_LANDSCAPE -> {
true
}
Configuration.ORIENTATION_PORTRAIT -> {
false
isTvSettings()
}
else -> {
false
}
}
nav_view?.isVisible = isNavVisible && !landscape
nav_rail_view?.isVisible = isNavVisible && landscape
binding?.apply {
navView.isVisible = isNavVisible && !landscape
navRailView.isVisible = isNavVisible && landscape
// Hide library on TV since it is not supported yet :(
val isTrueTv = isTrueTvSettings()
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
}
}
//private var mCastSession: CastSession? = null
@ -546,11 +577,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
CommonActivity.dispatchKeyEvent(this, event)?.let {
return it
val start = System.currentTimeMillis()
try {
val response = CommonActivity.dispatchKeyEvent(this, event)
if (response != null)
return response
} finally {
debugAssert({
val end = System.currentTimeMillis()
val delta = end - start
delta > 100
}) {
"Took over 100ms to navigate, smth is VERY wrong"
}
}
return super.dispatchKeyEvent(event)
}
@ -655,6 +698,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe {
pluginsLock.withLock {
synchronized(allProviders) {
// Load cloned sites after plugins have been loaded since clones depend on plugins.
try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
@ -680,6 +724,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
}
}
lateinit var viewModel: ResultViewModel2
@ -691,28 +736,217 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
private fun hidePreviewPopupDialog() {
viewModel.clear()
bottomPreviewPopup.dismissSafe(this)
bottomPreviewPopup = null
bottomPreviewBinding = null
}
var bottomPreviewPopup: BottomSheetDialog? = null
private fun showPreviewPopupDialog(): BottomSheetDialog {
val ret = (bottomPreviewPopup ?: run {
private var bottomPreviewPopup: BottomSheetDialog? = null
private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null
private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding {
val ret = (bottomPreviewBinding ?: run {
val builder =
BottomSheetDialog(this)
builder.setContentView(R.layout.bottom_resultview_preview)
val binding: BottomResultviewPreviewBinding =
BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false)
bottomPreviewBinding = binding
builder.setContentView(binding.root)
builder.setOnDismissListener {
bottomPreviewPopup = null
bottomPreviewBinding = null
viewModel.clear()
}
builder.setCanceledOnTouchOutside(true)
builder.show()
builder
bottomPreviewPopup = builder
binding
})
bottomPreviewPopup = ret
return ret
}
private var binding: ActivityMainBinding? = null
object TvFocus {
data class FocusTarget(
val width: Int,
val height: Int,
val x: Float,
val y: Float,
) {
companion object {
fun lerp(a: FocusTarget, b: FocusTarget, lerp: Float): FocusTarget {
val ilerp = 1 - lerp
return FocusTarget(
width = (a.width * ilerp + b.width * lerp).toInt(),
height = (a.height * ilerp + b.height * lerp).toInt(),
x = a.x * ilerp + b.x * lerp,
y = a.y * ilerp + b.y * lerp
)
}
}
}
var last: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
var current: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
var focusOutline: WeakReference<View> = WeakReference(null)
var lastFocus: WeakReference<View> = WeakReference(null)
private val layoutListener: View.OnLayoutChangeListener =
View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
updateFocusView(
v, same = true
)
}
private val attachListener: View.OnAttachStateChangeListener =
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
updateFocusView(v)
}
override fun onViewDetachedFromWindow(v: View) {
// removes the focus view but not the listener as updateFocusView(null) will remove the listener
focusOutline.get()?.isVisible = false
}
}
private fun setTargetPosition(target: FocusTarget) {
focusOutline.get()?.apply {
layoutParams = layoutParams?.apply {
width = target.width
height = target.height
}
translationX = target.x
translationY = target.y
bringToFront()
}
}
private var animator: ValueAnimator? = null
@MainThread
fun updateFocusView(newFocus: View?, same: Boolean = false) {
val focusOutline = focusOutline.get() ?: return
lastFocus.get()?.apply {
removeOnLayoutChangeListener(layoutListener)
removeOnAttachStateChangeListener(attachListener)
}
val wasGone = focusOutline.isGone
val visible =
newFocus != null && newFocus.measuredHeight > 0 && newFocus.measuredWidth > 0 && newFocus.isShown && newFocus.tag != "tv_no_focus_tag"
focusOutline.isVisible = visible
if (newFocus != null) {
lastFocus = WeakReference(newFocus)
val out = IntArray(2)
newFocus.getLocationInWindow(out)
val (screenX, screenY) = out
var (x, y) = screenX.toFloat() to screenY.toFloat()
val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY
// println(">><<< $x $y $currentX $currentY")
if (!newFocus.isLtr()) {
x = x - focusOutline.rootView.width + newFocus.measuredWidth
}
// out of bounds = 0,0
if (screenX == 0 && screenY == 0) {
focusOutline.isVisible = false
}
newFocus.addOnLayoutChangeListener(layoutListener)
newFocus.addOnAttachStateChangeListener(attachListener)
val start = FocusTarget(
x = currentX,
y = currentY,
width = focusOutline.measuredWidth,
height = focusOutline.measuredHeight
)
val end = FocusTarget(
x = x,
y = y,
width = newFocus.measuredWidth,
height = newFocus.measuredHeight
)
// if they are the same within then snap, aka scrolling
val deltaMin = 50.toPx
if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) {
animator?.cancel()
last = start
current = end
setTargetPosition(end)
return
}
// if running then "reuse"
if (animator?.isRunning == true) {
current = end
return
} else {
animator?.cancel()
}
last = start
current = end
// if previously gone, then tp
if (wasGone) {
setTargetPosition(current)
return
}
// animate between a and b
animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
startDelay = 0
duration = 100
addUpdateListener { animation ->
val animatedValue = animation.animatedValue as Float
val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
setTargetPosition(target)
}
start()
}
// post check
if (!same) {
newFocus.postDelayed({
updateFocusView(lastFocus.get(), same = true)
}, 200)
}
/*
the following is working, but somewhat bad code code
if (!wasGone) {
(focusOutline.parent as? ViewGroup)?.let {
TransitionManager.endTransitions(it)
TransitionManager.beginDelayedTransition(
it,
TransitionSet().addTransition(ChangeBounds())
.addTransition(ChangeTransform())
.setDuration(100)
)
}
}
focusOutline.layoutParams = focusOutline.layoutParams?.apply {
width = newFocus.measuredWidth
height = newFocus.measuredHeight
}
focusOutline.translationX = x.toFloat()
focusOutline.translationY = y.toFloat()*/
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
app.initClient(this)
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
@ -737,16 +971,48 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (isCastApiAvailable()) {
mSessionManager = CastContext.getSharedInstance(this).sessionManager
}
} catch (e: Exception) {
logError(e)
} catch (t: Throwable) {
logError(t)
}
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
updateTv()
// backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
try {
val appVer = BuildConfig.VERSION_NAME
val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: ""
if (appVer != lastAppAutoBackup) {
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
backup()
}
} catch (t: Throwable) {
logError(t)
}
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
binding = try {
if (isTvSettings()) {
setContentView(R.layout.activity_main_tv)
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 {
setContentView(R.layout.activity_main)
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())
@ -777,7 +1043,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (PluginManager.checkSafeModeFile()) {
normalSafeApiCall {
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
showToast(R.string.safe_mode_file, Toast.LENGTH_LONG)
}
} else if (lastError == null) {
ioSafe {
@ -831,41 +1097,44 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
observeNullable(viewModel.page) { resource ->
if (resource == null) {
bottomPreviewPopup.dismissSafe(this)
hidePreviewPopupDialog()
return@observeNullable
}
when (resource) {
is Resource.Failure -> {
showToast(this, R.string.error)
showToast(R.string.error)
viewModel.clear()
hidePreviewPopupDialog()
}
is Resource.Loading -> {
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = true
resultview_preview_result?.isVisible = false
resultview_preview_loading_shimmer?.startShimmer()
resultviewPreviewLoading.isVisible = true
resultviewPreviewResult.isVisible = false
resultviewPreviewLoadingShimmer.startShimmer()
}
}
is Resource.Success -> {
val d = resource.value
showPreviewPopupDialog().apply {
resultview_preview_loading?.isVisible = false
resultview_preview_result?.isVisible = true
resultview_preview_loading_shimmer?.stopShimmer()
resultviewPreviewLoading.isVisible = false
resultviewPreviewResult.isVisible = true
resultviewPreviewLoadingShimmer.stopShimmer()
resultview_preview_title?.text = d.title
resultviewPreviewTitle.text = d.title
resultview_preview_meta_type.setText(d.typeText)
resultview_preview_meta_year.setText(d.yearText)
resultview_preview_meta_duration.setText(d.durationText)
resultview_preview_meta_rating.setText(d.ratingText)
resultviewPreviewMetaType.setText(d.typeText)
resultviewPreviewMetaYear.setText(d.yearText)
resultviewPreviewMetaDuration.setText(d.durationText)
resultviewPreviewMetaRating.setText(d.ratingText)
resultview_preview_description?.setText(d.plotText)
resultview_preview_poster?.setImage(
resultviewPreviewDescription.setText(d.plotText)
resultviewPreviewPoster.setImage(
d.posterImage ?: d.posterBackgroundImage
)
resultview_preview_poster?.setOnClickListener {
resultviewPreviewPoster.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
val value = viewModel.watchStatus.value ?: WatchType.NONE
@ -876,12 +1145,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
bookmarksUpdatedEvent(true)
}
}
if (!isTvSettings()) // dont want this clickable on tv layout
resultview_preview_description?.setOnClickListener { view ->
resultviewPreviewDescription.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
@ -891,7 +1159,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}
}
resultview_preview_more_info?.setOnClickListener {
resultviewPreviewMoreInfo.setOnClickListener {
viewModel.clear()
hidePreviewPopupDialog()
lastPopup?.let {
loadSearchResult(it)
@ -932,7 +1201,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
ioSafe {
initAll()
// No duplicates (which can happen by registerMainAPI)
apis = allProviders.distinctBy { it }
apis = synchronized(allProviders) {
allProviders.distinctBy { it }
}
}
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
@ -963,22 +1234,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
.setPopExitAnim(R.anim.nav_pop_exit)
.setPopUpTo(navController.graph.startDestination, false)
.build()*/
nav_view?.setupWithNavController(navController)
val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
nav_rail?.setupWithNavController(navController)
binding?.navView?.setupWithNavController(navController)
val navRail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
navRail?.setupWithNavController(navController)
if (isTvSettings()) {
nav_rail?.background?.alpha = 200
navRail?.background?.alpha = 200
} else {
nav_rail?.background?.alpha = 255
navRail?.background?.alpha = 255
}
nav_rail?.setOnItemSelectedListener { item ->
navRail?.setOnItemSelectedListener { item ->
onNavDestinationSelected(
item,
navController
)
}
nav_view?.setOnItemSelectedListener { item ->
binding?.navView?.setOnItemSelectedListener { item ->
onNavDestinationSelected(
item,
navController
@ -1009,16 +1280,16 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}*/
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
binding?.navView?.itemRippleColor = rippleColor
navRail?.itemRippleColor = rippleColor
navRail?.itemActiveIndicatorColor = rippleColor
binding?.navView?.itemActiveIndicatorColor = rippleColor
if (!checkWrite()) {
requestRW()
if (checkWrite()) return
}
CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
//CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
// THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION
//if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) {
@ -1085,6 +1356,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
if (BuildConfig.DEBUG) {
var providersAndroidManifestString = "Current androidmanifest should be:\n"
synchronized(allProviders) {
for (api in allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix(
@ -1092,7 +1364,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
)
}\" android:pathPrefix=\"/\"/>\n"
}
}
println(providersAndroidManifestString)
}

View File

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

View File

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

View File

@ -57,32 +57,6 @@ fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) ->
liveData.observe(this) { action(it) }
}
inline fun <reified T : Any> some(value: T?): Some<T> {
return if (value == null) {
Some.None
} else {
Some.Success(value)
}
}
sealed class Some<out T> {
data class Success<out T>(val value: T) : Some<T>()
object None : Some<Nothing>()
override fun toString(): String {
return when (this) {
is None -> "None"
is Success -> "Some(${value.toString()})"
}
}
}
sealed class ResourceSome<out T> {
data class Success<out T>(val value: T) : ResourceSome<T>()
object None : ResourceSome<Nothing>()
data class Loading(val data: Any? = null) : ResourceSome<Nothing>()
}
sealed class Resource<out T> {
data class Success<out T>(val value: T) : Resource<T>()
data class Failure(
@ -155,19 +129,14 @@ fun CoroutineScope.launchSafe(
return this.launch(context, start, obj)
}
suspend fun <T> safeApiCall(
apiCall: suspend () -> T,
fun<T> throwAbleToResource(
throwable: Throwable
): Resource<T> {
return withContext(Dispatchers.IO) {
try {
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
logError(throwable)
when (throwable) {
return when (throwable) {
is NullPointerException -> {
for (line in throwable.stackTrace) {
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
return@withContext Resource.Failure(
return Resource.Failure(
false,
null,
null,
@ -194,7 +163,7 @@ suspend fun <T> safeApiCall(
)
}
is UnknownHostException -> {
Resource.Failure(true, null, null, "Cannot connect to server, try again later.")
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
}
is ErrorLoadingException -> {
Resource.Failure(
@ -215,8 +184,24 @@ suspend fun <T> safeApiCall(
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
)
}
is CancellationException -> {
throwable.cause?.let {
throwAbleToResource(it)
} ?: safeFail(throwable)
}
else -> safeFail(throwable)
}
}
suspend fun <T> safeApiCall(
apiCall: suspend () -> T,
): Resource<T> {
return withContext(Dispatchers.IO) {
try {
Resource.Success(apiCall.invoke())
} catch (throwable: Throwable) {
logError(throwable)
throwAbleToResource(throwable)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,20 +8,29 @@ import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.APIHolder.getId
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.FragmentHomeHeadBinding
import com.lagradost.cloudstream3.databinding.FragmentHomeHeadTvBinding
import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage
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.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
@ -32,118 +41,51 @@ import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectSt
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.activity_main.view.*
import kotlinx.android.synthetic.main.fragment_home_head.view.*
import kotlinx.android.synthetic.main.fragment_home_head.view.home_bookmarked_child_recyclerview
import kotlinx.android.synthetic.main.fragment_home_head.view.home_watch_parent_item_title
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_bookmarked_holder
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_none_padding
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_plan_to_watch_btt
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.fragment_home_head_tv.view.home_type_completed_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_dropped_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_on_hold_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_type_watching_btt
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_child_recyclerview
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_watch_holder
import kotlinx.android.synthetic.main.toast.view.*
class HomeParentItemAdapterPreview(
items: MutableList<HomeViewModel.ExpandableHomepageList>,
val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
expandCallback: ((String) -> Unit)? = null,
private val loadCallback: (LoadClickCallback) -> Unit,
private val loadMoreCallback: (() -> Unit),
private val changeHomePageCallback: ((View) -> Unit),
private val reloadStored: (() -> Unit),
private val loadStoredData: ((Set<WatchType>) -> Unit),
private val searchQueryCallback: ((Pair<Boolean, String>) -> Unit)
) : ParentItemAdapter(items, clickCallback, moreInfoClickCallback, expandCallback) {
private var previewData: Resource<Pair<Boolean, List<LoadResponse>>> = Resource.Loading()
private var resumeWatchingData: List<SearchResponse> = listOf()
private var bookmarkData: Pair<Boolean, List<SearchResponse>> =
false to listOf()
private var apiName: String = "NONE"
private val viewModel: HomeViewModel,
) : ParentItemAdapter(items, clickCallback = {
viewModel.click(it)
}, moreInfoClickCallback = {
viewModel.popup(it)
}, expandCallback = {
viewModel.expand(it)
}) {
val headItems = 1
private var availableWatchStatusTypes: Pair<Set<WatchType>, Set<WatchType>> =
setOf<WatchType>() to setOf()
fun setAvailableWatchStatusTypes(data: Pair<Set<WatchType>, Set<WatchType>>) {
availableWatchStatusTypes = data
holder?.setAvailableWatchStatusTypes(data)
}
companion object {
private const val VIEW_TYPE_HEADER = 2
private const val VIEW_TYPE_ITEM = 1
}
fun setResumeWatchingData(resumeWatching: List<SearchResponse>) {
resumeWatchingData = resumeWatching
holder?.updateResume(resumeWatchingData)
}
fun setPreviewData(preview: Resource<Pair<Boolean, List<LoadResponse>>>) {
previewData = preview
holder?.updatePreview(preview)
}
fun setApiName(name: String) {
apiName = name
holder?.updateApiName(name)
}
fun setBookmarkData(data: Pair<Boolean, List<SearchResponse>>) {
bookmarkData = data
holder?.updateBookmarks(data)
}
override fun getItemViewType(position: Int) = when (position) {
0 -> VIEW_TYPE_HEADER
else -> VIEW_TYPE_ITEM
}
var holder: HeaderViewHolder? = null
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> {
holder.updatePreview(previewData)
holder.updateResume(resumeWatchingData)
holder.updateBookmarks(bookmarkData)
holder.setAvailableWatchStatusTypes(availableWatchStatusTypes)
holder.updateApiName(apiName)
}
else -> super.onBindViewHolder(holder, position - 1)
is HeaderViewHolder -> {}
else -> super.onBindViewHolder(holder, position - headItems)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
println("onCreateViewHolder $viewType")
return when (viewType) {
VIEW_TYPE_HEADER -> HeaderViewHolder(
LayoutInflater.from(parent.context).inflate(
if (isTvSettings()) R.layout.fragment_home_head_tv else R.layout.fragment_home_head,
VIEW_TYPE_HEADER -> {
val inflater = LayoutInflater.from(parent.context)
val binding = if (isTvSettings()) FragmentHomeHeadTvBinding.inflate(
inflater,
parent,
false
),
loadCallback,
loadMoreCallback,
changeHomePageCallback,
clickCallback,
reloadStored,
loadStoredData,
searchQueryCallback,
moreInfoClickCallback
).also {
this.holder = it
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
HeaderViewHolder(
binding,
viewModel,
)
}
VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType)
else -> error("Unhandled viewType=$viewType")
}
@ -154,7 +96,7 @@ class HomeParentItemAdapterPreview(
}
override fun getItemId(position: Int): Long {
if (position == 0) return previewData.hashCode().toLong()
if (position == 0) return 0//previewData.hashCode().toLong()
return super.getItemId(position - headItems)
}
@ -163,6 +105,7 @@ class HomeParentItemAdapterPreview(
is HeaderViewHolder -> {
holder.onViewDetachedFromWindow()
}
else -> super.onViewDetachedFromWindow(holder)
}
}
@ -172,246 +115,24 @@ class HomeParentItemAdapterPreview(
is HeaderViewHolder -> {
holder.onViewAttachedToWindow()
}
else -> super.onViewAttachedToWindow(holder)
}
}
class HeaderViewHolder
constructor(
itemView: View,
private val clickCallback: ((LoadClickCallback) -> Unit)?,
private val loadMoreCallback: (() -> Unit),
private val changeHomePageCallback: ((View) -> Unit),
private val searchClickCallback: (SearchClickCallback) -> Unit,
private val reloadStored: () -> Unit,
private val loadStoredData: ((Set<WatchType>) -> Unit),
private val searchQueryCallback: ((Pair<Boolean, String>) -> Unit),
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit
) : RecyclerView.ViewHolder(itemView) {
private var previewAdapter: HomeScrollAdapter? = null
private val previewViewpager: ViewPager2? = itemView.home_preview_viewpager
private val previewHeader: FrameLayout? = itemView.home_preview
private val previewCallback: ViewPager2.OnPageChangeCallback =
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
// home_search?.isIconified = true
//home_search?.isVisible = true
//home_search?.clearFocus()
previewAdapter?.apply {
if (position >= itemCount - 1 && hasMoreItems) {
hasMoreItems = false // dont make two requests
loadMoreCallback()
//homeViewModel.loadMoreHomeScrollResponses()
}
}
previewAdapter?.getItem(position)
?.apply {
//itemView.home_preview_title_holder?.let { parent ->
// TransitionManager.beginDelayedTransition(
// parent,
// ChangeBounds()
// )
//}
itemView.home_preview_description?.isGone =
this.plot.isNullOrBlank()
itemView.home_preview_description?.text =
this.plot ?: ""
itemView.home_preview_text?.text = this.name
itemView.home_preview_tags?.apply {
removeAllViews()
tags?.forEach { tag ->
val chip = Chip(context)
val chipDrawable =
ChipDrawable.createFromAttributes(
context,
null,
0,
R.style.ChipFilledSemiTransparent
)
chip.setChipDrawable(chipDrawable)
chip.text = tag
chip.isChecked = false
chip.isCheckable = false
chip.isFocusable = false
chip.isClickable = false
addView(chip)
}
}
itemView.home_preview_tags?.isGone =
tags.isNullOrEmpty()
itemView.home_preview_image?.setImage(
posterUrl,
posterHeaders
)
// itemView.home_preview_title?.text = name
itemView.home_preview_play?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(
START_ACTION_RESUME_LATEST,
view,
position,
this
)
)
}
itemView.home_preview_info?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(0, view, position, this)
)
}
itemView.home_preview_play_btt?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(
START_ACTION_RESUME_LATEST,
view,
position,
this
)
)
}
// This makes the hidden next buttons only available when on the info button
// Otherwise you might be able to go to the next item without being at the info button
itemView.home_preview_info_btt?.setOnFocusChangeListener { _, hasFocus ->
itemView.home_preview_hidden_next_focus?.isFocusable = hasFocus
}
itemView.home_preview_play_btt?.setOnFocusChangeListener { _, hasFocus ->
itemView.home_preview_hidden_prev_focus?.isFocusable = hasFocus
}
itemView.home_preview_info_btt?.setOnClickListener { view ->
clickCallback?.invoke(
LoadClickCallback(0, view, position, this)
)
}
itemView.home_preview_hidden_next_focus?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
previewViewpager?.apply {
setCurrentItem(currentItem + 1, true)
}
itemView.home_preview_info_btt?.requestFocus()
}
}
itemView.home_preview_hidden_prev_focus?.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
previewViewpager?.apply {
if (currentItem <= 0) {
nav_rail_view?.menu?.getItem(0)?.actionView?.requestFocus()
} else {
setCurrentItem(currentItem - 1, true)
itemView.home_preview_play_btt?.requestFocus()
}
}
}
}
// very ugly code, but I dont care
val watchType =
DataStoreHelper.getResultWatchState(this.getId())
itemView.home_preview_bookmark?.setText(watchType.stringRes)
itemView.home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
itemView.home_preview_bookmark.context,
watchType.iconRes
),
null,
null
)
itemView.home_preview_bookmark?.setOnClickListener { fab ->
fab.context.getActivity()?.showBottomDialog(
WatchType.values()
.map { fab.context.getString(it.stringRes) }
.toList(),
DataStoreHelper.getResultWatchState(this.getId()).ordinal,
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
val newValue = WatchType.values()[it]
itemView.home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
itemView.home_preview_bookmark.context,
newValue.iconRes
),
null,
null
)
itemView.home_preview_bookmark?.setText(newValue.stringRes)
ResultViewModel2.updateWatchStatus(
this,
newValue
)
reloadStored()
}
}
}
}
}
private var resumeAdapter: HomeChildItemAdapter? = null
private var resumeHolder: View? = itemView.home_watch_holder
private var resumeRecyclerView: RecyclerView? = itemView.home_watch_child_recyclerview
private var bookmarkHolder: View? = itemView.home_bookmarked_holder
private var bookmarkAdapter: HomeChildItemAdapter? = null
private var bookmarkRecyclerView: RecyclerView? =
itemView.home_bookmarked_child_recyclerview
fun onViewDetachedFromWindow() {
previewViewpager?.unregisterOnPageChangeCallback(previewCallback)
}
fun onViewAttachedToWindow() {
previewViewpager?.registerOnPageChangeCallback(previewCallback)
}
private val toggleList = listOf(
Pair(itemView.home_type_watching_btt, WatchType.WATCHING),
Pair(itemView.home_type_completed_btt, WatchType.COMPLETED),
Pair(itemView.home_type_dropped_btt, WatchType.DROPPED),
Pair(itemView.home_type_on_hold_btt, WatchType.ONHOLD),
Pair(itemView.home_plan_to_watch_btt, WatchType.PLANTOWATCH),
)
init {
itemView.home_preview_change_api?.setOnClickListener { view ->
changeHomePageCallback(view)
}
itemView.home_preview_change_api2?.setOnClickListener { view ->
changeHomePageCallback(view)
}
previewViewpager?.apply {
//if (!isTvSettings())
setPageTransformer(HomeScrollTransformer())
//else
// setPageTransformer(null)
if (adapter == null)
adapter = HomeScrollAdapter(
if (isTvSettings()) R.layout.home_scroll_view_tv else R.layout.home_scroll_view,
if (isTvSettings()) true else null
)
}
previewAdapter = previewViewpager?.adapter as? HomeScrollAdapter?
// previewViewpager?.registerOnPageChangeCallback(previewCallback)
if (resumeAdapter == null) {
resumeRecyclerView?.adapter = HomeChildItemAdapter(
val binding: ViewBinding,
val viewModel: HomeViewModel,
) : RecyclerView.ViewHolder(binding.root) {
private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter()
private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
ArrayList(),
nextFocusUp = itemView.nextFocusUpId,
nextFocusDown = itemView.nextFocusDownId
) { callback ->
if (callback.action != SEARCH_ACTION_SHOW_METADATA) {
searchClickCallback(callback)
viewModel.click(callback)
return@HomeChildItemAdapter
}
callback.view.context?.getActivity()?.showOptionSelectStringRes(
@ -430,7 +151,7 @@ class HomeParentItemAdapterPreview(
when (actionId + if (isTv) 0 else 1) {
// play
0 -> {
searchClickCallback.invoke(
viewModel.click(
SearchClickCallback(
START_ACTION_RESUME_LATEST,
callback.view,
@ -438,11 +159,10 @@ class HomeParentItemAdapterPreview(
callback.card
)
)
reloadStored()
}
//info
1 -> {
searchClickCallback(
viewModel.click(
SearchClickCallback(
SEARCH_ACTION_LOAD,
callback.view,
@ -450,30 +170,25 @@ class HomeParentItemAdapterPreview(
callback.card
)
)
reloadStored()
}
// remove
2 -> {
val card = callback.card
if (card is DataStoreHelper.ResumeWatchingResult) {
DataStoreHelper.removeLastWatched(card.parentId)
reloadStored()
viewModel.reloadStored()
}
}
}
}
}
}
resumeAdapter = resumeRecyclerView?.adapter as? HomeChildItemAdapter
if (bookmarkAdapter == null) {
bookmarkRecyclerView?.adapter = HomeChildItemAdapter(
private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
ArrayList(),
nextFocusUp = itemView.nextFocusUpId,
nextFocusDown = itemView.nextFocusDownId
) { callback ->
if (callback.action != SEARCH_ACTION_SHOW_METADATA) {
searchClickCallback(callback)
viewModel.click(callback)
return@HomeChildItemAdapter
}
callback.view.context?.getActivity()?.showOptionSelectStringRes(
@ -491,7 +206,7 @@ class HomeParentItemAdapterPreview(
) { (isTv, actionId) ->
when (actionId + if (isTv) 0 else 1) { // play
0 -> {
searchClickCallback.invoke(
viewModel.click(
SearchClickCallback(
START_ACTION_RESUME_LATEST,
callback.view,
@ -499,10 +214,10 @@ class HomeParentItemAdapterPreview(
callback.card
)
)
reloadStored()
}
1 -> { // info
searchClickCallback(
viewModel.click(
SearchClickCallback(
SEARCH_ACTION_LOAD,
callback.view,
@ -510,110 +225,338 @@ class HomeParentItemAdapterPreview(
callback.card
)
)
reloadStored()
}
2 -> { // remove
DataStoreHelper.setResultWatchState(
callback.card.id,
WatchType.NONE.internalId
)
reloadStored()
viewModel.reloadStored()
}
}
}
}
}
bookmarkAdapter = bookmarkRecyclerView?.adapter as? HomeChildItemAdapter
for ((chip, watch) in toggleList) {
chip?.isChecked = false
chip?.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
loadStoredData(
setOf(watch)
// If we filter all buttons then two can be checked at the same time
// Revert this if you want to go back to multi selection
// toggleList.filter { it.first?.isChecked == true }.map { it.second }.toSet()
private val previewViewpager: ViewPager2 =
itemView.findViewById(R.id.home_preview_viewpager)
private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
private var resumeRecyclerView: RecyclerView =
itemView.findViewById(R.id.home_watch_child_recyclerview)
private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
private var bookmarkRecyclerView: RecyclerView =
itemView.findViewById(R.id.home_bookmarked_child_recyclerview)
private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding)
private val previewCallback: ViewPager2.OnPageChangeCallback =
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
previewAdapter.apply {
if (position >= itemCount - 1 && hasMoreItems) {
hasMoreItems = false // don't make two requests
viewModel.loadMoreHomeScrollResponses()
}
}
val item = previewAdapter.getItem(position) ?: return
onSelect(item, position)
}
}
fun onSelect(item: LoadResponse, position: Int) {
(binding as? FragmentHomeHeadTvBinding)?.apply {
homePreviewDescription.isGone =
item.plot.isNullOrBlank()
homePreviewDescription.text =
item.plot ?: ""
homePreviewText.text = item.name
homePreviewTags.apply {
removeAllViews()
item.tags?.forEach { tag ->
val chip = Chip(context)
val chipDrawable =
ChipDrawable.createFromAttributes(
context,
null,
0,
R.style.ChipFilledSemiTransparent
)
chip.setChipDrawable(chipDrawable)
chip.text = tag
chip.isChecked = false
chip.isCheckable = false
chip.isFocusable = false
chip.isClickable = false
addView(chip)
}
}
homePreviewTags.isGone =
item.tags.isNullOrEmpty()
homePreviewPlayBtt.setOnClickListener { view ->
viewModel.click(
LoadClickCallback(
START_ACTION_RESUME_LATEST,
view,
position,
item
)
)
}
homePreviewInfoBtt.setOnClickListener { view ->
viewModel.click(
LoadClickCallback(0, view, position, item)
)
}
}
(binding as? FragmentHomeHeadBinding)?.apply {
homePreviewImage.setImage(item.posterUrl, item.posterHeaders)
homePreviewPlay.setOnClickListener { view ->
viewModel.click(
LoadClickCallback(
START_ACTION_RESUME_LATEST,
view,
position,
item
)
)
}
homePreviewInfo.setOnClickListener { view ->
viewModel.click(
LoadClickCallback(0, view, position, item)
)
}
// very ugly code, but I don't care
val id = item.getId()
val watchType =
DataStoreHelper.getResultWatchState(id)
homePreviewBookmark.setText(watchType.stringRes)
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
watchType.iconRes
),
null,
null
)
homePreviewBookmark.setOnClickListener { fab ->
fab.context.getActivity()?.showBottomDialog(
WatchType.values()
.map { fab.context.getString(it.stringRes) }
.toList(),
DataStoreHelper.getResultWatchState(id).ordinal,
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
val newValue = WatchType.values()[it]
homePreviewBookmark.setCompoundDrawablesWithIntrinsicBounds(
null,
ContextCompat.getDrawable(
homePreviewBookmark.context,
newValue.iconRes
),
null,
null
)
homePreviewBookmark.setText(newValue.stringRes)
ResultViewModel2.updateWatchStatus(
item,
newValue
)
}
}
}
}
fun onViewDetachedFromWindow() {
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
}
fun onViewAttachedToWindow() {
previewViewpager.registerOnPageChangeCallback(previewCallback)
binding.root.findViewTreeLifecycleOwner()?.apply {
observe(viewModel.preview) {
updatePreview(it)
}
if (binding is FragmentHomeHeadTvBinding) {
observe(viewModel.apiName) { name ->
binding.homePreviewChangeApi.text = name
binding.homePreviewChangeApi2.text = name
}
}
observe(viewModel.resumeWatching) {
updateResume(it)
}
observe(viewModel.bookmarks) {
updateBookmarks(it)
}
observe(viewModel.availableWatchStatusTypes) { (checked, visible) ->
for ((chip, watch) in toggleList) {
chip.apply {
isVisible = visible.contains(watch)
isChecked = checked.contains(watch)
}
}
}
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
}
private val toggleList = listOf<Pair<Chip, WatchType>>(
Pair(itemView.findViewById(R.id.home_type_watching_btt), WatchType.WATCHING),
Pair(itemView.findViewById(R.id.home_type_completed_btt), WatchType.COMPLETED),
Pair(itemView.findViewById(R.id.home_type_dropped_btt), WatchType.DROPPED),
Pair(itemView.findViewById(R.id.home_type_on_hold_btt), WatchType.ONHOLD),
Pair(itemView.findViewById(R.id.home_plan_to_watch_btt), WatchType.PLANTOWATCH),
)
init {
previewViewpager.setPageTransformer(HomeScrollTransformer())
previewViewpager.adapter = previewAdapter
resumeRecyclerView.adapter = resumeAdapter
bookmarkRecyclerView.adapter = bookmarkAdapter
resumeRecyclerView.setLinearListLayout()
bookmarkRecyclerView.setLinearListLayout()
for ((chip, watch) in toggleList) {
chip.isChecked = false
chip.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
viewModel.loadStoredData(setOf(watch))
}
// Else if all are unchecked -> Do not load data
else if (toggleList.all { it.first?.isChecked != true }) {
loadStoredData(emptySet())
else if (toggleList.all { !it.first.isChecked }) {
viewModel.loadStoredData(emptySet())
}
}
}
itemView.home_search?.context?.fixPaddingStatusbar(itemView.home_search)
(binding as? FragmentHomeHeadTvBinding)?.apply {
homePreviewChangeApi.setOnClickListener { view ->
view.context.selectHomepage(viewModel.repo?.name) { api ->
viewModel.loadAndCancel(api, forceReload = true, fromUI = true)
}
}
homePreviewChangeApi2.setOnClickListener { view ->
view.context.selectHomepage(viewModel.repo?.name) { api ->
viewModel.loadAndCancel(api, forceReload = true, fromUI = true)
}
}
itemView.home_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
// This makes the hidden next buttons only available when on the info button
// Otherwise you might be able to go to the next item without being at the info button
homePreviewInfoBtt.setOnFocusChangeListener { _, hasFocus ->
homePreviewHiddenNextFocus.isFocusable = hasFocus
}
homePreviewPlayBtt.setOnFocusChangeListener { _, hasFocus ->
homePreviewHiddenPrevFocus.isFocusable = hasFocus
}
homePreviewHiddenNextFocus.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
previewViewpager.setCurrentItem(previewViewpager.currentItem + 1, true)
homePreviewInfoBtt.requestFocus()
}
}
homePreviewHiddenPrevFocus.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
previewViewpager.apply {
if (currentItem <= 0) {
findViewById<NavigationRailView?>(R.id.nav_rail_view)?.menu?.getItem(
0
)?.actionView?.requestFocus()
} else {
setCurrentItem(currentItem - 1, true)
binding.homePreviewPlayBtt.requestFocus()
}
}
}
}
}
(binding as? FragmentHomeHeadBinding)?.apply {
fixPaddingStatusbar(binding.homeSearch)
homeSearch.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
searchQueryCallback.invoke(false to query)
//QuickSearchFragment.pushSearch(activity, query, currentApiName?.let { arrayOf(it) }
viewModel.queryTextSubmit(query)
return true
}
override fun onQueryTextChange(newText: String): Boolean {
searchQueryCallback.invoke(true to newText)
//searchViewModel.quickSearch(newText)
viewModel.queryTextChange(newText)
return true
}
})
}
fun updateApiName(name: String) {
itemView.home_preview_change_api2?.text = name
itemView.home_preview_change_api?.text = name
}
fun updatePreview(preview: Resource<Pair<Boolean, List<LoadResponse>>>) {
itemView.home_preview_change_api2?.isGone = preview is Resource.Success
private fun updatePreview(preview: Resource<Pair<Boolean, List<LoadResponse>>>) {
if (binding is FragmentHomeHeadTvBinding) {
binding.homePreviewChangeApi2.isGone = preview is Resource.Success
}
if (preview is Resource.Success) {
itemView.home_none_padding?.apply {
homeNonePadding.apply {
val params = layoutParams
params.height = 0
layoutParams = params
}
} else {
itemView.home_none_padding?.context?.fixPaddingStatusbarView(itemView.home_none_padding)
fixPaddingStatusbarView(homeNonePadding)
}
when (preview) {
is Resource.Success -> {
if (true != previewAdapter?.setItems(
if (!previewAdapter.setItems(
preview.value.second,
preview.value.first
)
) {
// this might seam weird and useless, however this prevents a very weird andrid bug were the viewpager is not rendered properly
// I have no idea why that happens, but this is my ducktape solution
previewViewpager?.setCurrentItem(0, false)
previewViewpager?.beginFakeDrag()
previewViewpager?.fakeDragBy(1f)
previewViewpager?.endFakeDrag()
previewViewpager.setCurrentItem(0, false)
previewViewpager.beginFakeDrag()
previewViewpager.fakeDragBy(1f)
previewViewpager.endFakeDrag()
previewCallback.onPageSelected(0)
previewHeader?.isVisible = true
previewHeader.isVisible = true
}
}
else -> {
previewAdapter?.setItems(listOf(), false)
previewViewpager?.setCurrentItem(0, false)
previewHeader?.isVisible = false
previewAdapter.setItems(listOf(), false)
previewViewpager.setCurrentItem(0, false)
previewHeader.isVisible = false
}
}
// previewViewpager?.postDelayed({ previewViewpager?.scr(100, 0) }, 1000)
//previewViewpager?.postInvalidate()
}
fun updateResume(resumeWatching: List<SearchResponse>) {
resumeHolder?.isVisible = resumeWatching.isNotEmpty()
resumeAdapter?.updateList(resumeWatching)
private fun updateResume(resumeWatching: List<SearchResponse>) {
resumeHolder.isVisible = resumeWatching.isNotEmpty()
resumeAdapter.updateList(resumeWatching)
if (!isTvSettings()) {
itemView.home_watch_parent_item_title?.setOnClickListener {
moreInfoClickCallback.invoke(
if (binding is FragmentHomeHeadBinding) {
binding.homeBookmarkParentItemTitle.setOnClickListener {
viewModel.popup(
HomeViewModel.ExpandableHomepageList(
HomePageList(
itemView.home_watch_parent_item_title?.text.toString(),
binding.homeWatchParentItemTitle.text.toString(),
resumeWatching,
false
), 1, false
@ -623,21 +566,23 @@ class HomeParentItemAdapterPreview(
}
}
fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
bookmarkHolder?.isVisible = data.first
bookmarkAdapter?.updateList(data.second)
if (!isTvSettings()) {
itemView.home_bookmark_parent_item_title?.setOnClickListener {
val items = toggleList.mapNotNull { it.first }.filter { it.isChecked }
private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
val (visible, list) = data
bookmarkHolder.isVisible = visible
bookmarkAdapter.updateList(list)
if (binding is FragmentHomeHeadBinding) {
binding.homeBookmarkParentItemTitle.setOnClickListener {
val items = toggleList.map { it.first }.filter { it.isChecked }
if (items.isEmpty()) return@setOnClickListener // we don't want to show an empty dialog
val textSum = items
.mapNotNull { it.text }.joinToString()
moreInfoClickCallback.invoke(
viewModel.popup(
HomeViewModel.ExpandableHomepageList(
HomePageList(
textSum,
data.second,
list,
false
), 1, false
)
@ -645,14 +590,5 @@ class HomeParentItemAdapterPreview(
}
}
}
fun setAvailableWatchStatusTypes(availableWatchStatusTypes: Pair<Set<WatchType>, Set<WatchType>>) {
for ((chip, watch) in toggleList) {
chip?.apply {
isVisible = availableWatchStatusTypes.second.contains(watch)
isChecked = availableWatchStatusTypes.first.contains(watch)
}
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,6 +12,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.Toast
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
@ -24,6 +27,7 @@ import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.ui.SubtitleView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
@ -42,8 +46,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.fragment_player.*
import kotlinx.android.synthetic.main.player_custom_layout.*
enum class PlayerResize(@StringRes val nameRes: Int) {
Fit(R.string.resize_fit),
@ -72,9 +74,15 @@ abstract class AbstractPlayerFragment(
var isBuffering = true
protected open var hasPipModeSupport = true
var playerPausePlayHolderHolder : FrameLayout? = null
var playerPausePlay : ImageView? = null
var playerBuffering : ProgressBar? = null
var playerView : PlayerView? = null
var piphide : FrameLayout? = null
var subtitleHolder : FrameLayout? = null
@LayoutRes
protected var layout: Int = R.layout.fragment_player
protected open var layout: Int = R.layout.fragment_player
open fun nextEpisode() {
throw NotImplementedError()
@ -133,15 +141,15 @@ abstract class AbstractPlayerFragment(
isBuffering = CSPlayerLoading.IsBuffering == isPlaying
if (isBuffering) {
player_pause_play_holder_holder?.isVisible = false
player_buffering?.isVisible = true
playerPausePlayHolderHolder?.isVisible = false
playerBuffering?.isVisible = true
} else {
player_pause_play_holder_holder?.isVisible = true
player_buffering?.isVisible = false
playerPausePlayHolderHolder?.isVisible = true
playerBuffering?.isVisible = false
if (wasPlaying != isPlaying) {
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
val drawable = player_pause_play?.drawable
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
val drawable = playerPausePlay?.drawable
var startedAnimation = false
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
@ -163,10 +171,10 @@ abstract class AbstractPlayerFragment(
// somehow the phone is wacked
if (!startedAnimation) {
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
}
} else {
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
}
}
@ -243,14 +251,12 @@ abstract class AbstractPlayerFragment(
fun showToast(message: String, gotoNext: Boolean = false) {
if (gotoNext && hasNextMirror()) {
showToast(
activity,
message,
Toast.LENGTH_SHORT
)
nextMirror()
} else {
showToast(
activity,
context?.getString(R.string.no_links_found_toast) + "\n" + message,
Toast.LENGTH_LONG
)
@ -328,9 +334,9 @@ abstract class AbstractPlayerFragment(
}
// Necessary for multiple combined videos
player_view?.setShowMultiWindowTimeBar(true)
player_view?.player = player
player_view?.performClick()
playerView?.setShowMultiWindowTimeBar(true)
playerView?.player = player
playerView?.performClick()
}
}
@ -388,9 +394,9 @@ abstract class AbstractPlayerFragment(
)
if (player is CS3IPlayer) {
subView = player_view?.findViewById(R.id.exo_subtitles)
subView = playerView?.findViewById(R.id.exo_subtitles)
subStyle = SubtitlesFragment.getCurrentSavedStyle()
player.initSubtitles(subView, subtitle_holder, subStyle)
player.initSubtitles(subView, subtitleHolder, subStyle)
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
@ -458,10 +464,10 @@ abstract class AbstractPlayerFragment(
PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
PlayerResize.Zoom -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
}
player_view?.resizeMode = type
playerView?.resizeMode = type
if (showToast)
showToast(activity, resize.nameRes, Toast.LENGTH_SHORT)
showToast(resize.nameRes, Toast.LENGTH_SHORT)
}
override fun onStop() {
@ -482,6 +488,13 @@ abstract class AbstractPlayerFragment(
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(layout, container, false)
val root = inflater.inflate(layout, container, false)
playerPausePlayHolderHolder = root.findViewById(R.id.player_pause_play_holder_holder)
playerPausePlay = root.findViewById(R.id.player_pause_play)
playerBuffering = root.findViewById(R.id.player_buffering)
playerView = root.findViewById(R.id.player_view)
piphide = root.findViewById(R.id.piphide)
subtitleHolder = root.findViewById(R.id.subtitle_holder)
return root
}
}

View File

@ -52,7 +52,15 @@ import javax.net.ssl.SSLSession
const val TAG = "CS3ExoPlayer"
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language"
/** Cache */
/** toleranceBeforeUs The maximum time that the actual position seeked to may precede the
* requested seek position, in microseconds. Must be non-negative. */
const val toleranceBeforeUs = 300_000L
/**
* toleranceAfterUs The maximum time that the actual position seeked to may exceed the requested
* seek position, in microseconds. Must be non-negative.
*/
const val toleranceAfterUs = 300_000L
class CS3IPlayer : IPlayer {
private var isPlaying = false
@ -387,6 +395,7 @@ class CS3IPlayer : IPlayer {
Log.i(TAG, "setPreferredSubtitles REQUIRES_RELOAD")
return@let true
}
SubtitleStatus.IS_ACTIVE -> {
Log.i(TAG, "setPreferredSubtitles IS_ACTIVE")
@ -412,6 +421,7 @@ class CS3IPlayer : IPlayer {
// }, 1)
//}
}
SubtitleStatus.NOT_FOUND -> {
Log.i(TAG, "setPreferredSubtitles NOT_FOUND")
return@let true
@ -702,7 +712,7 @@ class CS3IPlayer : IPlayer {
)
)
// Allows any seeking to be +- 0.3s to allow for faster seeking
.setSeekParameters(SeekParameters(300_000, 300_000))
.setSeekParameters(SeekParameters(toleranceBeforeUs, toleranceAfterUs))
.setLoadControl(
DefaultLoadControl.Builder()
.setTargetBufferBytes(
@ -769,7 +779,7 @@ class CS3IPlayer : IPlayer {
private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? {
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
for (lastTimeStamp in lastTimeStamps) {
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) {
if (lastTimeStamp.startMs <= position && (position + (toleranceBeforeUs / 1000L) + 1) < lastTimeStamp.endMs) {
return lastTimeStamp
}
}
@ -777,11 +787,12 @@ class CS3IPlayer : IPlayer {
}
fun updatedTime(writePosition: Long? = null) {
getCurrentTimestamp(writePosition)?.let { timestamp ->
val position = writePosition ?: exoPlayer?.currentPosition
getCurrentTimestamp(position)?.let { timestamp ->
onTimestampInvoked?.invoke(timestamp)
}
val position = writePosition ?: exoPlayer?.currentPosition
val duration = exoPlayer?.contentDuration
if (duration != null && position != null) {
playerPositionChanged?.invoke(Pair(position, duration))
@ -810,9 +821,11 @@ class CS3IPlayer : IPlayer {
CSPlayerEvent.Play -> {
play()
}
CSPlayerEvent.Pause -> {
pause()
}
CSPlayerEvent.ToggleMute -> {
if (volume <= 0) {
//is muted
@ -823,6 +836,7 @@ class CS3IPlayer : IPlayer {
volume = 0f
}
}
CSPlayerEvent.PlayPauseToggle -> {
if (isPlaying) {
pause()
@ -830,6 +844,7 @@ class CS3IPlayer : IPlayer {
play()
}
}
CSPlayerEvent.SeekForward -> seekTime(seekActionTime)
CSPlayerEvent.SeekBack -> seekTime(-seekActionTime)
CSPlayerEvent.NextEpisode -> nextEpisode?.invoke()
@ -954,6 +969,7 @@ class CS3IPlayer : IPlayer {
Player.STATE_READY -> {
onRenderFirst()
}
else -> {}
}
@ -963,6 +979,7 @@ class CS3IPlayer : IPlayer {
Player.STATE_READY -> {
}
Player.STATE_ENDED -> {
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(context)
@ -974,12 +991,15 @@ class CS3IPlayer : IPlayer {
handleEvent(CSPlayerEvent.NextEpisode)
}
}
Player.STATE_BUFFERING -> {
updatedTime()
}
Player.STATE_IDLE -> {
// IDLE
}
else -> Unit
}
}
@ -994,11 +1014,13 @@ class CS3IPlayer : IPlayer {
&& exoPlayer?.duration != TIME_UNSET -> {
exoPlayer?.prepare()
}
error.errorCode == PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> {
// Re-initialize player at the current live window default position.
exoPlayer?.seekToDefaultPosition()
exoPlayer?.prepare()
}
else -> {
playerError?.invoke(error)
}
@ -1025,6 +1047,7 @@ class CS3IPlayer : IPlayer {
Player.STATE_READY -> {
}
Player.STATE_ENDED -> {
// Only play next episode if autoplay is on (default)
if (PreferenceManager.getDefaultSharedPreferences(context)
@ -1036,12 +1059,15 @@ class CS3IPlayer : IPlayer {
handleEvent(CSPlayerEvent.NextEpisode)
}
}
Player.STATE_BUFFERING -> {
updatedTime()
}
Player.STATE_IDLE -> {
// IDLE
}
else -> Unit
}
}
@ -1052,9 +1078,9 @@ class CS3IPlayer : IPlayer {
}
override fun onRenderedFirstFrame() {
updatedTime()
super.onRenderedFirstFrame()
onRenderFirst()
updatedTime()
}
})
} catch (e: Exception) {
@ -1082,8 +1108,11 @@ class CS3IPlayer : IPlayer {
}
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
return
}
Log.i(TAG, "Rendered first frame")
hasUsedFirstRender = true
val invalid = exoPlayer?.duration?.let { duration ->
// Only errors short playback when not playing downloaded files
duration < 20_000L && currentDownloadedFile == null
@ -1100,7 +1129,6 @@ class CS3IPlayer : IPlayer {
}
setPreferredSubtitles(currentSubtitles)
hasUsedFirstRender = true
val format = exoPlayer?.videoFormat
val width = format?.width
val height = format?.height
@ -1113,7 +1141,7 @@ class CS3IPlayer : IPlayer {
updatedTime()
}
.setLooper(Looper.getMainLooper())
.setPosition( /* positionMs= */contentDuration * percentage / 100)
.setPosition(contentDuration * percentage / 100)
// .setPayload(customPayloadData)
.setDeleteAfterDelivery(false)
.send()
@ -1121,7 +1149,6 @@ class CS3IPlayer : IPlayer {
}
}
}
}
private fun loadOfflinePlayer(context: Context, data: ExtractorUri) {
Log.i(TAG, "loadOfflinePlayer")
@ -1169,6 +1196,7 @@ class CS3IPlayer : IPlayer {
null
}
}
SubtitleOrigin.URL -> {
if (onlineSourceFactory != null) {
activeSubtitles.add(sub)
@ -1181,6 +1209,7 @@ class CS3IPlayer : IPlayer {
null
}
}
SubtitleOrigin.EMBEDDED_IN_VIDEO -> {
if (offlineSourceFactory != null) {
activeSubtitles.add(sub)

View File

@ -14,16 +14,15 @@ import android.provider.Settings
import android.text.Editable
import android.util.DisplayMetrics
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.blue
import androidx.core.graphics.green
@ -37,10 +36,13 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
import com.lagradost.cloudstream3.databinding.SubtitleOffsetBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvidersIsActive
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.utils.Qualities
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
@ -52,29 +54,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.showSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.toPx
import com.lagradost.cloudstream3.utils.Vector2
import kotlinx.android.synthetic.main.player_custom_layout.*
import kotlinx.android.synthetic.main.player_custom_layout.bottom_player_bar
import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd
import kotlinx.android.synthetic.main.player_custom_layout.exo_ffwd_text
import kotlinx.android.synthetic.main.player_custom_layout.exo_progress
import kotlinx.android.synthetic.main.player_custom_layout.exo_rew
import kotlinx.android.synthetic.main.player_custom_layout.exo_rew_text
import kotlinx.android.synthetic.main.player_custom_layout.player_center_menu
import kotlinx.android.synthetic.main.player_custom_layout.player_ffwd_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play
import kotlinx.android.synthetic.main.player_custom_layout.player_pause_play_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_left_icon
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_progressbar_right_icon
import kotlinx.android.synthetic.main.player_custom_layout.player_rew_holder
import kotlinx.android.synthetic.main.player_custom_layout.player_time_text
import kotlinx.android.synthetic.main.player_custom_layout.player_video_bar
import kotlinx.android.synthetic.main.player_custom_layout.shadow_overlay
import kotlinx.android.synthetic.main.trailer_custom_layout.*
import kotlin.math.*
const val MINIMUM_SEEK_TIME = 7000L // when swipe seeking
@ -93,16 +72,13 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
protected open var isFullScreenPlayer = true
protected open var isTv = false
protected var playerBinding: PlayerCustomLayoutBinding? = null
// state of player UI
protected var isShowing = false
protected var isLocked = false
//private var episodes: List<Any> = listOf()
protected fun setEpisodes(ep: List<Any>) {
//hasEpisodes = ep.size > 1 // if has 2 episodes or more because you dont want to switch to your current episode
//(player_episode_list?.adapter as? PlayerEpisodeAdapter?)?.updateList(ep)
}
protected var hasEpisodes = false
private set
//protected val hasEpisodes
@ -116,6 +92,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
* This will be set in runtime based on settings.
**/
protected var currentQualityProfile = 1
// protected var currentPrefQuality =
// Qualities.P2160.value // preferred maximum quality, used for ppl w bad internet or on cell
protected var fastForwardTime = 10000L
@ -184,6 +161,21 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
R.drawable.ic_baseline_volume_up_24,
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = super.onCreateView(inflater, container, savedInstanceState) ?: return null
playerBinding = PlayerCustomLayoutBinding.bind(root.findViewById(R.id.player_holder))
return root
}
override fun onDestroyView() {
playerBinding = null
super.onDestroyView()
}
open fun showMirrorsDialogue() {
throw NotImplementedError()
}
@ -216,24 +208,24 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (isShowing) {
updateUIVisibility()
} else {
player_holder?.postDelayed({ updateUIVisibility() }, 200)
playerBinding?.playerHolder?.postDelayed({ updateUIVisibility() }, 200)
}
val titleMove = if (isShowing) 0f else -50.toPx.toFloat()
player_video_title?.let {
playerBinding?.playerVideoTitle?.let {
ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
duration = 200
start()
}
}
player_video_title_rez?.let {
playerBinding?.playerVideoTitleRez?.let {
ObjectAnimator.ofFloat(it, "translationY", titleMove).apply {
duration = 200
start()
}
}
val playerBarMove = if (isShowing) 0f else 50.toPx.toFloat()
bottom_player_bar?.let {
playerBinding?.bottomPlayerBar?.let {
ObjectAnimator.ofFloat(it, "translationY", playerBarMove).apply {
duration = 200
start()
@ -249,7 +241,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
val sView = subView
val sStyle = subStyle
if (sView != null && sStyle != null) {
val move = if (isShowing) -((bottom_player_bar?.height?.toFloat()
val move = if (isShowing) -((playerBinding?.bottomPlayerBar?.height?.toFloat()
?: 0f) + 40.toPx) else -sStyle.elevation.toPx.toFloat()
ObjectAnimator.ofFloat(sView, "translationY", move).apply {
duration = 200
@ -258,23 +250,25 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
val playerSourceMove = if (isShowing) 0f else -50.toPx.toFloat()
player_open_source?.let {
playerBinding?.apply {
playerOpenSource.let {
ObjectAnimator.ofFloat(it, "translationY", playerSourceMove).apply {
duration = 200
start()
}
}
if (!isLocked) {
player_ffwd_holder?.alpha = 1f
player_rew_holder?.alpha = 1f
playerFfwdHolder.alpha = 1f
playerRewHolder.alpha = 1f
// player_pause_play_holder?.alpha = 1f
shadow_overlay?.isVisible = true
shadow_overlay?.startAnimation(fadeAnimation)
player_ffwd_holder?.startAnimation(fadeAnimation)
player_rew_holder?.startAnimation(fadeAnimation)
player_pause_play?.startAnimation(fadeAnimation)
shadowOverlay.isVisible = true
shadowOverlay.startAnimation(fadeAnimation)
playerFfwdHolder.startAnimation(fadeAnimation)
playerRewHolder.startAnimation(fadeAnimation)
playerPausePlay.startAnimation(fadeAnimation)
/*if (isBuffering) {
player_pause_play?.isVisible = false
@ -287,13 +281,15 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
//player_buffering?.startAnimation(fadeAnimation)
}
bottom_player_bar?.startAnimation(fadeAnimation)
player_open_source?.startAnimation(fadeAnimation)
player_top_holder?.startAnimation(fadeAnimation)
bottomPlayerBar.startAnimation(fadeAnimation)
playerOpenSource.startAnimation(fadeAnimation)
playerTopHolder.startAnimation(fadeAnimation)
}
}
override fun subtitlesChanged() {
player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null
playerBinding?.playerSubtitleOffsetBtt?.isGone =
player.getCurrentPreferredSubtitle() == null
}
protected fun enterFullscreen() {
@ -339,7 +335,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun setPlayBackSpeed(speed: Float) {
try {
setKey(PLAYBACK_SPEED_KEY, speed)
player_speed_btt?.text =
playerBinding?.playerSpeedBtt?.text =
getString(R.string.player_speed_text_format).format(speed)
.replace(".0x", "x")
} catch (e: Exception) {
@ -355,67 +351,68 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
private fun showSubtitleOffsetDialog() {
context?.let { ctx ->
val ctx = context ?: return
val binding = SubtitleOffsetBinding.inflate(LayoutInflater.from(ctx), null, false)
val builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
.setView(R.layout.subtitle_offset)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
val beforeOffset = subtitleDelay
val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
/*val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!!
val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!!
val input = dialog.findViewById<EditText>(R.id.subtitle_offset_input)!!
val sub = dialog.findViewById<ImageView>(R.id.subtitle_offset_subtract)!!
val subMore = dialog.findViewById<ImageView>(R.id.subtitle_offset_subtract_more)!!
val add = dialog.findViewById<ImageView>(R.id.subtitle_offset_add)!!
val addMore = dialog.findViewById<ImageView>(R.id.subtitle_offset_add_more)!!
val subTitle = dialog.findViewById<TextView>(R.id.subtitle_offset_sub_title)!!
val subTitle = dialog.findViewById<TextView>(R.id.subtitle_offset_sub_title)!!*/
binding.apply {
subtitleOffsetInput.doOnTextChanged { text, _, _, _ ->
text?.toString()?.toLongOrNull()?.let { time ->
subtitleDelay = time
val str = when {
time > 0L -> {
txt(R.string.subtitle_offset_extra_hint_later_format, time)
}
input.doOnTextChanged { text, _, _, _ ->
text?.toString()?.toLongOrNull()?.let {
subtitleDelay = it
when {
it > 0L -> {
context?.getString(R.string.subtitle_offset_extra_hint_later_format)
?.format(it)
}
it < 0L -> {
context?.getString(R.string.subtitle_offset_extra_hint_before_format)
?.format(-it)
}
it == 0L -> {
context?.getString(R.string.subtitle_offset_extra_hint_none_format)
time < 0L -> {
txt(R.string.subtitle_offset_extra_hint_before_format, -time)
}
else -> {
null
}
}?.let { str ->
subTitle.text = str
txt(R.string.subtitle_offset_extra_hint_none_format)
}
}
subtitleOffsetSubTitle.setText(str)
}
input.text = Editable.Factory.getInstance()?.newEditable(beforeOffset.toString())
}
subtitleOffsetInput.text =
Editable.Factory.getInstance()?.newEditable(beforeOffset.toString())
val buttonChange = 100L
val buttonChangeMore = 1000L
fun changeBy(by: Long) {
val current = (input.text?.toString()?.toLongOrNull() ?: 0) + by
input.text = Editable.Factory.getInstance()?.newEditable(current.toString())
val current = (subtitleOffsetInput.text?.toString()?.toLongOrNull() ?: 0) + by
subtitleOffsetInput.text =
Editable.Factory.getInstance()?.newEditable(current.toString())
}
add.setOnClickListener {
subtitleOffsetAdd.setOnClickListener {
changeBy(buttonChange)
}
addMore.setOnClickListener {
subtitleOffsetAddMore.setOnClickListener {
changeBy(buttonChangeMore)
}
sub.setOnClickListener {
subtitleOffsetSubtract.setOnClickListener {
changeBy(-buttonChange)
}
subMore.setOnClickListener {
subtitleOffsetSubtractMore.setOnClickListener {
changeBy(-buttonChangeMore)
}
@ -423,17 +420,18 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (isFullScreenPlayer)
activity?.hideSystemUI()
}
applyButton.setOnClickListener {
applyBtt.setOnClickListener {
dialog.dismissSafe(activity)
player.seekTime(1L)
}
cancelButton.setOnClickListener {
cancelBtt.setOnClickListener {
subtitleDelay = beforeOffset
dialog.dismissSafe(activity)
}
}
}
private fun showSpeedDialog() {
val speedsText =
listOf(
@ -470,22 +468,23 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
fun resetRewindText() {
exo_rew_text?.text =
playerBinding?.exoRewText?.text =
getString(R.string.rew_text_regular_format).format(fastForwardTime / 1000)
}
fun resetFastForwardText() {
exo_ffwd_text?.text =
playerBinding?.exoFfwdText?.text =
getString(R.string.ffw_text_regular_format).format(fastForwardTime / 1000)
}
private fun rewind() {
try {
player_center_menu?.isGone = false
player_rew_holder?.alpha = 1f
playerBinding?.apply {
playerCenterMenu.isGone = false
playerRewHolder.alpha = 1f
val rotateLeft = AnimationUtils.loadAnimation(context, R.anim.rotate_left)
exo_rew?.startAnimation(rotateLeft)
exoRew.startAnimation(rotateLeft)
val goLeft = AnimationUtils.loadAnimation(context, R.anim.go_left)
goLeft.setAnimationListener(object : Animation.AnimationListener {
@ -494,15 +493,17 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
override fun onAnimationRepeat(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
exo_rew_text?.post {
exoRewText.post {
resetRewindText()
player_center_menu?.isGone = !isShowing
player_rew_holder?.alpha = if (isShowing) 1f else 0f
playerCenterMenu.isGone = !isShowing
playerRewHolder.alpha = if (isShowing) 1f else 0f
}
}
})
exo_rew_text?.startAnimation(goLeft)
exo_rew_text?.text = getString(R.string.rew_text_format).format(fastForwardTime / 1000)
exoRewText.startAnimation(goLeft)
exoRewText.text =
getString(R.string.rew_text_format).format(fastForwardTime / 1000)
}
player.seekTime(-fastForwardTime)
} catch (e: Exception) {
logError(e)
@ -511,11 +512,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun fastForward() {
try {
player_center_menu?.isGone = false
player_ffwd_holder?.alpha = 1f
playerBinding?.apply {
playerCenterMenu.isGone = false
playerFfwdHolder.alpha = 1f
val rotateRight = AnimationUtils.loadAnimation(context, R.anim.rotate_right)
exo_ffwd?.startAnimation(rotateRight)
exoFfwd.startAnimation(rotateRight)
val goRight = AnimationUtils.loadAnimation(context, R.anim.go_right)
goRight.setAnimationListener(object : Animation.AnimationListener {
@ -524,15 +526,17 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
override fun onAnimationRepeat(animation: Animation?) {}
override fun onAnimationEnd(animation: Animation?) {
exo_ffwd_text?.post {
exoFfwdText.post {
resetFastForwardText()
player_center_menu?.isGone = !isShowing
player_ffwd_holder?.alpha = if (isShowing) 1f else 0f
playerCenterMenu.isGone = !isShowing
playerFfwdHolder.alpha = if (isShowing) 1f else 0f
}
}
})
exo_ffwd_text?.startAnimation(goRight)
exo_ffwd_text?.text = getString(R.string.ffw_text_format).format(fastForwardTime / 1000)
exoFfwdText.startAnimation(goRight)
exoFfwdText.text =
getString(R.string.ffw_text_format).format(fastForwardTime / 1000)
}
player.seekTime(fastForwardTime)
} catch (e: Exception) {
logError(e)
@ -542,13 +546,13 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun onClickChange() {
isShowing = !isShowing
if (isShowing) {
player_intro_play?.isGone = true
playerBinding?.playerIntroPlay?.isGone = true
autoHide()
}
if (isFullScreenPlayer)
activity?.hideSystemUI()
animateLayoutChanges()
player_pause_play?.requestFocus()
playerBinding?.playerPausePlay?.requestFocus()
}
private fun toggleLock() {
@ -558,7 +562,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
isLocked = !isLocked
if (isLocked && isShowing) {
player_holder?.postDelayed({
playerBinding?.playerHolder?.postDelayed({
if (isLocked && isShowing) {
onClickChange()
}
@ -566,8 +570,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
val fadeTo = if (isLocked) 0f else 1f
val fadeAnimation = AlphaAnimation(player_video_title.alpha, fadeTo).apply {
playerBinding?.apply {
val fadeAnimation = AlphaAnimation(playerVideoTitle.alpha, fadeTo).apply {
duration = 100
fillAfter = true
}
@ -575,9 +579,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
updateUIVisibility()
// MENUS
//centerMenu.startAnimation(fadeAnimation)
player_pause_play?.startAnimation(fadeAnimation)
player_ffwd_holder?.startAnimation(fadeAnimation)
player_rew_holder?.startAnimation(fadeAnimation)
playerPausePlay.startAnimation(fadeAnimation)
playerFfwdHolder.startAnimation(fadeAnimation)
playerRewHolder.startAnimation(fadeAnimation)
//if (hasEpisodes)
// player_episodes_button?.startAnimation(fadeAnimation)
@ -585,17 +589,17 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
//video_bar.startAnimation(fadeAnimation)
//TITLE
player_video_title_rez?.startAnimation(fadeAnimation)
player_episode_filler?.startAnimation(fadeAnimation)
player_video_title?.startAnimation(fadeAnimation)
player_top_holder?.startAnimation(fadeAnimation)
playerVideoTitleRez.startAnimation(fadeAnimation)
playerEpisodeFiller.startAnimation(fadeAnimation)
playerVideoTitle.startAnimation(fadeAnimation)
playerTopHolder.startAnimation(fadeAnimation)
// BOTTOM
player_lock_holder?.startAnimation(fadeAnimation)
playerLockHolder.startAnimation(fadeAnimation)
//player_go_back_holder?.startAnimation(fadeAnimation)
shadow_overlay?.isVisible = true
shadow_overlay?.startAnimation(fadeAnimation)
shadowOverlay.isVisible = true
shadowOverlay.startAnimation(fadeAnimation)
}
updateLockUI()
}
@ -609,43 +613,48 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
togglePlayerTitleGone = true
}
}
player_lock_holder?.isGone = isGone
player_video_bar?.isGone = isGone
player_pause_play_holder?.isGone = isGone
player_pause_play?.isGone = isGone
playerBinding?.apply {
playerLockHolder.isGone = isGone
playerVideoBar.isGone = isGone
playerPausePlay.isGone = isGone
//player_buffering?.isGone = isGone
player_top_holder?.isGone = isGone
playerTopHolder.isGone = isGone
//player_episodes_button?.isVisible = !isGone && hasEpisodes
player_video_title?.isGone = togglePlayerTitleGone
playerVideoTitle.isGone = togglePlayerTitleGone
// player_video_title_rez?.isGone = isGone
player_episode_filler?.isGone = isGone
player_center_menu?.isGone = isGone
player_lock?.isGone = !isShowing
playerEpisodeFiller.isGone = isGone
playerCenterMenu.isGone = isGone
playerLock.isGone = !isShowing
//player_media_route_button?.isClickable = !isGone
player_go_back_holder?.isGone = isGone
player_sources_btt?.isGone = isGone
player_skip_episode?.isClickable = !isGone
playerGoBackHolder.isGone = isGone
playerSourcesBtt.isGone = isGone
playerSkipEpisode.isClickable = !isGone
}
}
private fun updateLockUI() {
player_lock?.setIconResource(if (isLocked) R.drawable.video_locked else R.drawable.video_unlocked)
playerBinding?.apply {
playerLock.setIconResource(if (isLocked) R.drawable.video_locked else R.drawable.video_unlocked)
if (layout == R.layout.fragment_player) {
val color = if (isLocked) context?.colorFromAttribute(R.attr.colorPrimary)
else Color.WHITE
if (color != null) {
player_lock?.setTextColor(color)
player_lock?.iconTint = ColorStateList.valueOf(color)
player_lock?.rippleColor =
playerLock.setTextColor(color)
playerLock.iconTint = ColorStateList.valueOf(color)
playerLock.rippleColor =
ColorStateList.valueOf(Color.argb(50, color.red, color.green, color.blue))
}
}
}
}
private var currentTapIndex = 0
protected fun autoHide() {
currentTapIndex++
val index = currentTapIndex
player_holder?.postDelayed({
playerBinding?.playerHolder?.postDelayed({
if (!isCurrentTouchValid && isShowing && index == currentTapIndex && player.getIsPlaying()) {
onClickChange()
}
@ -657,7 +666,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private fun toggleShowDelayed() {
if (doubleTapEnabled || doubleTapPauseEnabled) {
val index = currentDoubleTapIndex
player_holder?.postDelayed({
playerBinding?.playerHolder?.postDelayed({
if (index == currentDoubleTapIndex) {
onClickChange()
}
@ -788,7 +797,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (event == null || view == null) return false
val currentTouch = Vector2(event.x, event.y)
val startTouch = currentTouchStart
player_intro_play?.isGone = true
playerBinding?.apply {
playerIntroPlay.isGone = true
when (event.action) {
MotionEvent.ACTION_DOWN -> {
// validates if the touch is inside of the player area
@ -814,13 +826,18 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
}
}
MotionEvent.ACTION_UP -> {
if (isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
// seek time
if (swipeHorizontalEnabled && currentTouchAction == TouchAction.Time) {
val startTime = currentTouchStartPlayerTime
if (startTime != null) {
calculateNewTime(startTime, startTouch, currentTouch)?.let { seekTo ->
calculateNewTime(
startTime,
startTouch,
currentTouch
)?.let { seekTo ->
if (abs(seekTo - startTime) > MINIMUM_SEEK_TIME) {
player.seekTo(seekTo)
}
@ -850,10 +867,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
if (doubleTapEnabled)
rewind()
}
currentTouch.x > screenWidth / 2 + (DOUBLE_TAB_PAUSE_PERCENTAGE * screenWidth) -> {
if (doubleTapEnabled)
fastForward()
}
else -> {
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
}
@ -889,11 +908,13 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
currentTouchStartTime = null
// resets UI
player_time_text?.isVisible = false
player_progressbar_left_holder?.isVisible = false
player_progressbar_right_holder?.isVisible = false
playerTimeText.isVisible = false
playerProgressbarLeftHolder.isVisible = false
playerProgressbarRightHolder.isVisible = false
currentLastTouchEndTime = System.currentTimeMillis()
}
MotionEvent.ACTION_MOVE -> {
// if current touch is valid
if (startTouch != null && isCurrentTouchValid && !isLocked && isFullScreenPlayer) {
@ -932,9 +953,9 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
diffFromLast.y * VERTICAL_MULTIPLIER / screenHeight.toFloat()
// update UI
player_time_text?.isVisible = false
player_progressbar_left_holder?.isVisible = false
player_progressbar_right_holder?.isVisible = false
playerTimeText.isVisible = false
playerProgressbarLeftHolder.isVisible = false
playerProgressbarRightHolder.isVisible = false
when (currentTouchAction) {
TouchAction.Time -> {
@ -949,16 +970,19 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
currentTouch
)?.let { newMs ->
val skipMs = newMs - startTime
player_time_text?.text =
playerTimeText.apply {
text =
"${convertTimeToString(newMs / 1000)} [${
(if (abs(skipMs) < 1000) "" else (if (skipMs > 0) "+" else "-"))
}${convertTimeToString(abs(skipMs / 1000))}]"
player_time_text?.isVisible = true
isVisible = true
}
}
}
}
TouchAction.Brightness -> {
player_progressbar_right_holder?.isVisible = true
playerProgressbarRightHolder.isVisible = true
val lastRequested = currentRequestedBrightness
currentRequestedBrightness =
min(
@ -971,11 +995,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
setBrightness(currentRequestedBrightness)
// max is set high to make it smooth
player_progressbar_right?.max = 100_000
player_progressbar_right?.progress =
playerProgressbarRight.max = 100_000
playerProgressbarRight.progress =
max(2_000, (currentRequestedBrightness * 100_000f).toInt())
player_progressbar_right_icon?.setImageResource(
playerProgressbarRightIcon.setImageResource(
brightnessIcons[min( // clamp the value just in case
brightnessIcons.size - 1,
max(
@ -985,9 +1009,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
)]
)
}
TouchAction.Volume -> {
(activity?.getSystemService(Context.AUDIO_SERVICE) as? AudioManager)?.let { audioManager ->
player_progressbar_left_holder?.isVisible = true
playerProgressbarLeftHolder.isVisible = true
val maxVolume =
audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
val currentVolume =
@ -1001,11 +1026,11 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
)
// max is set high to make it smooth
player_progressbar_left?.max = 100_000
player_progressbar_left?.progress =
playerProgressbarLeft.max = 100_000
playerProgressbarLeft.progress =
max(2_000, (currentRequestedVolume * 100_000f).toInt())
player_progressbar_left_icon?.setImageResource(
playerProgressbarLeftIcon.setImageResource(
volumeIcons[min( // clamp the value just in case
volumeIcons.size - 1,
max(
@ -1030,12 +1055,14 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
}
}
}
else -> Unit
}
}
}
}
}
}
currentTouchLast = currentTouch
return true
}
@ -1055,26 +1082,29 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
return true
}
}
KeyEvent.KEYCODE_DPAD_UP -> {
if (!isShowing) {
onClickChange()
return true
}
}
KeyEvent.KEYCODE_DPAD_LEFT -> {
if (!isShowing && !isLocked) {
player.seekTime(-androidTVInterfaceOffSeekTime)
return true
} else if (player_pause_play?.isFocused == true) {
} else if (playerBinding?.playerPausePlay?.isFocused == true) {
player.seekTime(-androidTVInterfaceOnSeekTime)
return true
}
}
KeyEvent.KEYCODE_DPAD_RIGHT -> {
if (!isShowing && !isLocked) {
player.seekTime(androidTVInterfaceOffSeekTime)
return true
} else if (player_pause_play?.isFocused == true) {
} else if (playerBinding?.playerPausePlay?.isFocused == true) {
player.seekTime(androidTVInterfaceOnSeekTime)
return true
}
@ -1117,11 +1147,12 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
isShowing = false
// if nothing has loaded these buttons should not be visible
player_skip_episode?.isVisible = false
player_tracks_btt?.isVisible = false
player_skip_op?.isVisible = false
shadow_overlay?.isVisible = false
playerBinding?.apply {
playerSkipEpisode.isVisible = false
playerTracksBtt.isVisible = false
playerSkipOp.isVisible = false
shadowOverlay.isVisible = false
}
updateLockUI()
updateUIVisibility()
animateLayoutChanges()
@ -1150,50 +1181,65 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
PlayerEventType.Lock -> {
toggleLock()
}
PlayerEventType.NextEpisode -> {
player.handleEvent(CSPlayerEvent.NextEpisode)
}
PlayerEventType.Pause -> {
player.handleEvent(CSPlayerEvent.Pause)
}
PlayerEventType.PlayPauseToggle -> {
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
}
PlayerEventType.Play -> {
player.handleEvent(CSPlayerEvent.Play)
}
PlayerEventType.SkipCurrentChapter -> {
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
}
PlayerEventType.Resize -> {
nextResize()
}
PlayerEventType.PrevEpisode -> {
player.handleEvent(CSPlayerEvent.PrevEpisode)
}
PlayerEventType.SeekForward -> {
player.handleEvent(CSPlayerEvent.SeekForward)
}
PlayerEventType.ShowSpeed -> {
showSpeedDialog()
}
PlayerEventType.SeekBack -> {
player.handleEvent(CSPlayerEvent.SeekBack)
}
PlayerEventType.ToggleMute -> {
player.handleEvent(CSPlayerEvent.ToggleMute)
}
PlayerEventType.ToggleHide -> {
onClickChange()
}
PlayerEventType.ShowMirrors -> {
showMirrorsDialogue()
}
PlayerEventType.SearchSubtitlesOnline -> {
if (subsProvidersIsActive) {
openOnlineSubPicker(view.context, null) {}
}
}
PlayerEventType.SkipOp -> {
skipOp()
}
@ -1288,94 +1334,98 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// useSystemBrightness =
// settingsManager.getBoolean(ctx.getString(R.string.use_system_brightness_key), false)
}
player_speed_btt?.isVisible = playBackSpeedEnabled
player_resize_btt?.isVisible = playerResizeEnabled
playerBinding?.apply {
playerSpeedBtt.isVisible = playBackSpeedEnabled
playerResizeBtt.isVisible = playerResizeEnabled
}
} catch (e: Exception) {
logError(e)
}
player_pause_play?.setOnClickListener {
playerBinding?.apply {
playerPausePlay.setOnClickListener {
autoHide()
player.handleEvent(CSPlayerEvent.PlayPauseToggle)
}
skip_chapter_button?.setOnClickListener {
skipChapterButton.setOnClickListener {
player.handleEvent(CSPlayerEvent.SkipCurrentChapter)
}
// init clicks
player_resize_btt?.setOnClickListener {
playerResizeBtt.setOnClickListener {
autoHide()
nextResize()
}
player_speed_btt?.setOnClickListener {
playerSpeedBtt.setOnClickListener {
autoHide()
showSpeedDialog()
}
player_skip_op?.setOnClickListener {
playerSkipOp.setOnClickListener {
autoHide()
skipOp()
}
player_skip_episode?.setOnClickListener {
playerSkipEpisode.setOnClickListener {
autoHide()
player.handleEvent(CSPlayerEvent.NextEpisode)
}
player_lock?.setOnClickListener {
playerLock.setOnClickListener {
autoHide()
toggleLock()
}
player_subtitle_offset_btt?.setOnClickListener {
playerSubtitleOffsetBtt.setOnClickListener {
showSubtitleOffsetDialog()
}
exo_rew?.setOnClickListener {
exoRew.setOnClickListener {
autoHide()
rewind()
}
exo_ffwd?.setOnClickListener {
exoFfwd.setOnClickListener {
autoHide()
fastForward()
}
player_go_back?.setOnClickListener {
playerGoBack.setOnClickListener {
activity?.popCurrentPage()
}
player_sources_btt?.setOnClickListener {
playerSourcesBtt.setOnClickListener {
showMirrorsDialogue()
}
player_tracks_btt?.setOnClickListener {
playerTracksBtt.setOnClickListener {
showTracksDialogue()
}
// it is !not! a bug that you cant touch the right side, it does not register inputs on navbar or status bar
player_holder?.setOnTouchListener { callView, event ->
playerHolder.setOnTouchListener { callView, event ->
return@setOnTouchListener handleMotionEvent(callView, event)
}
exo_progress?.setOnTouchListener { _, event ->
exoProgress.setOnTouchListener { _, event ->
// this makes the bar not disappear when sliding
when (event.action) {
MotionEvent.ACTION_DOWN -> {
currentTapIndex++
}
MotionEvent.ACTION_MOVE -> {
currentTapIndex++
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_BUTTON_RELEASE -> {
autoHide()
}
}
return@setOnTouchListener false
}
}
// init UI
try {
uiReset()

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,16 @@
package com.lagradost.cloudstream3.ui.player.source_priority
import android.app.Dialog
import android.view.View
import android.widget.TextView
import androidx.annotation.StyleRes
import androidx.core.view.isVisible
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.PlayerQualityProfileDialogBinding
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import kotlinx.android.synthetic.main.player_quality_profile_dialog.*
class QualityProfileDialog(
val activity: FragmentActivity,
@ -24,32 +20,35 @@ class QualityProfileDialog(
private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit
) : Dialog(activity, themeRes) {
override fun show() {
setContentView(R.layout.player_quality_profile_dialog)
val profilesRecyclerView: RecyclerView = profiles_recyclerview
val binding = PlayerQualityProfileDialogBinding.inflate(this.layoutInflater, null, false)
setContentView(binding.root)//R.layout.player_quality_profile_dialog)
/*val profilesRecyclerView: RecyclerView = profiles_recyclerview
val useBtt: View = use_btt
val editBtt: View = edit_btt
val cancelBtt: View = cancel_btt
val defaultBtt: View = set_default_btt
val currentProfileText: TextView = currently_selected_profile_text
val selectedItemActionsHolder: View = selected_item_holder
val selectedItemActionsHolder: View = selected_item_holder*/
binding.apply {
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())
currentlySelectedProfileText.text = getProfileName(usedProfile).asString(context)
(profilesRecyclerview.adapter as? ProfilesAdapter)?.updateList(getProfiles())
}
profilesRecyclerView.adapter = ProfilesAdapter(
profilesRecyclerview.adapter = ProfilesAdapter(
mutableListOf(),
usedProfile,
) { oldIndex: Int?, newIndex: Int ->
profilesRecyclerView.adapter?.notifyItemChanged(newIndex)
selectedItemActionsHolder.alpha = 1f
profilesRecyclerview.adapter?.notifyItemChanged(newIndex)
selectedItemHolder.alpha = 1f
if (oldIndex != null) {
profilesRecyclerView.adapter?.notifyItemChanged(oldIndex)
profilesRecyclerview.adapter?.notifyItemChanged(oldIndex)
}
}
@ -64,7 +63,7 @@ class QualityProfileDialog(
}
defaultBtt.setOnClickListener {
setDefaultBtt.setOnClickListener {
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
val choices = QualityDataHelper.QualityProfileType.values()
.filter { it != QualityDataHelper.QualityProfileType.None }
@ -91,16 +90,16 @@ class QualityProfileDialog(
}
cancelBtt.setOnClickListener {
this.dismissSafe()
this@QualityProfileDialog.dismissSafe()
}
useBtt.setOnClickListener {
getCurrentProfile()?.let {
profileSelectionCallback.invoke(it)
this.dismissSafe()
this@QualityProfileDialog.dismissSafe()
}
}
}
super.show()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,112 +1,18 @@
package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.Intent.*
import android.content.res.ColorStateList
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AbsListView
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.ViewModelProvider
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.discord.panels.OverlappingPanelsLayout
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.services.SubscriptionWorkManager
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.EpisodeAdapter.Companion.getPlayerAction
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result.result_cast_items
import kotlinx.android.synthetic.main.fragment_result.result_cast_text
import kotlinx.android.synthetic.main.fragment_result.result_coming_soon
import kotlinx.android.synthetic.main.fragment_result.result_data_holder
import kotlinx.android.synthetic.main.fragment_result.result_description
import kotlinx.android.synthetic.main.fragment_result.result_download_movie
import kotlinx.android.synthetic.main.fragment_result.result_episode_loading
import kotlinx.android.synthetic.main.fragment_result.result_episodes
import kotlinx.android.synthetic.main.fragment_result.result_error_text
import kotlinx.android.synthetic.main.fragment_result.result_finish_loading
import kotlinx.android.synthetic.main.fragment_result.result_info
import kotlinx.android.synthetic.main.fragment_result.result_loading
import kotlinx.android.synthetic.main.fragment_result.result_loading_error
import kotlinx.android.synthetic.main.fragment_result.result_meta_duration
import kotlinx.android.synthetic.main.fragment_result.result_meta_rating
import kotlinx.android.synthetic.main.fragment_result.result_meta_site
import kotlinx.android.synthetic.main.fragment_result.result_meta_type
import kotlinx.android.synthetic.main.fragment_result.result_meta_year
import kotlinx.android.synthetic.main.fragment_result.result_movie_download_icon
import kotlinx.android.synthetic.main.fragment_result.result_movie_download_text
import kotlinx.android.synthetic.main.fragment_result.result_movie_download_text_precentage
import kotlinx.android.synthetic.main.fragment_result.result_movie_progress_downloaded
import kotlinx.android.synthetic.main.fragment_result.result_movie_progress_downloaded_holder
import kotlinx.android.synthetic.main.fragment_result.result_next_airing
import kotlinx.android.synthetic.main.fragment_result.result_next_airing_time
import kotlinx.android.synthetic.main.fragment_result.result_no_episodes
import kotlinx.android.synthetic.main.fragment_result.result_play_movie
import kotlinx.android.synthetic.main.fragment_result.result_poster
import kotlinx.android.synthetic.main.fragment_result.result_poster_holder
import kotlinx.android.synthetic.main.fragment_result.result_reload_connection_open_in_browser
import kotlinx.android.synthetic.main.fragment_result.result_reload_connectionerror
import kotlinx.android.synthetic.main.fragment_result.result_resume_parent
import kotlinx.android.synthetic.main.fragment_result.result_resume_progress_holder
import kotlinx.android.synthetic.main.fragment_result.result_resume_series_progress
import kotlinx.android.synthetic.main.fragment_result.result_resume_series_progress_text
import kotlinx.android.synthetic.main.fragment_result.result_resume_series_title
import kotlinx.android.synthetic.main.fragment_result.result_tag
import kotlinx.android.synthetic.main.fragment_result.result_tag_holder
import kotlinx.android.synthetic.main.fragment_result.result_title
import kotlinx.android.synthetic.main.fragment_result.result_vpn
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import com.lagradost.cloudstream3.utils.Event
const val START_ACTION_RESUME_LATEST = 1
const val START_ACTION_LOAD_EP = 2
@ -206,15 +112,14 @@ fun ResultEpisode.getWatchProgress(): Float {
return (getDisplayPosition() / 1000).toFloat() / (duration / 1000).toFloat()
}
open class ResultFragment : ResultTrailerPlayer() {
companion object {
const val URL_BUNDLE = "url"
const val API_NAME_BUNDLE = "apiName"
const val SEASON_BUNDLE = "season"
const val EPISODE_BUNDLE = "episode"
const val START_ACTION_BUNDLE = "startAction"
const val START_VALUE_BUNDLE = "startValue"
const val RESTART_BUNDLE = "restart"
object ResultFragment {
private const val URL_BUNDLE = "url"
private const val API_NAME_BUNDLE = "apiName"
private const val SEASON_BUNDLE = "season"
private const val EPISODE_BUNDLE = "episode"
private const val START_ACTION_BUNDLE = "startAction"
private const val START_VALUE_BUNDLE = "startValue"
private const val RESTART_BUNDLE = "restart"
fun newInstance(
card: SearchResponse, startAction: Int = 0, startValue: Int? = null
@ -252,38 +157,31 @@ open class ResultFragment : ResultTrailerPlayer() {
}
}
fun updateUI() {
updateUIListener?.invoke()
fun updateUI(id: Int? = null) {
// updateUIListener?.invoke()
updateUIEvent.invoke(id)
}
val updateUIEvent = Event<Int?>()
private var updateUIListener: (() -> Unit)? = null
}
//private var updateUIListener: (() -> Unit)? = null
open fun setTrailers(trailers: List<ExtractorLink>?) {}
protected lateinit var viewModel: ResultViewModel2 //by activityViewModels()
protected lateinit var syncModel: SyncViewModel
protected open val resultLayout = R.layout.fragment_result_swipe
//protected open val resultLayout = R.layout.fragment_result_swipe
/* override var layout = R.layout.fragment_result_swipe
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
viewModel =
ViewModelProvider(this)[ResultViewModel2::class.java]
syncModel =
ViewModelProvider(this)[SyncViewModel::class.java]
return inflater.inflate(resultLayout, container, false)
return super.onCreateView(inflater, container, savedInstanceState)
//return inflater.inflate(resultLayout, container, false)
}
private var downloadButton: EasyDownloadButton? = null
override fun onDestroyView() {
updateUIListener = null
(result_episodes?.adapter as? EpisodeAdapter)?.killAdapter()
downloadButton?.dispose()
super.onDestroyView()
}
@ -301,191 +199,26 @@ open class ResultFragment : ResultTrailerPlayer() {
super.onDestroy()
}
/// 0 = LOADING, 1 = ERROR LOADING, 2 = LOADED
private fun updateVisStatus(state: Int) {
when (state) {
0 -> {
result_bookmark_fab?.isGone = true
result_loading?.isVisible = true
result_finish_loading?.isVisible = false
result_loading_error?.isVisible = false
}
1 -> {
result_bookmark_fab?.isGone = true
result_loading?.isVisible = false
result_finish_loading?.isVisible = false
result_loading_error?.isVisible = true
result_reload_connection_open_in_browser?.isVisible = true
}
2 -> {
result_bookmark_fab?.isGone = isTrueTvSettings()
result_bookmark_fab?.extend()
//if (result_bookmark_button?.context?.isTrueTvSettings() == true) {
// when {
// result_play_movie?.isVisible == true -> {
// result_play_movie?.requestFocus()
// }
// result_resume_series_button?.isVisible == true -> {
// result_resume_series_button?.requestFocus()
// }
// else -> {
// result_bookmark_button?.requestFocus()
// }
// }
//}
result_loading?.isVisible = false
result_finish_loading?.isVisible = true
result_loading_error?.isVisible = false
}
}
}
open fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
}
private fun updateUI() {
syncModel.updateUserData()
viewModel.reloadEpisodes()
}
open fun updateMovie(data: ResourceSome<Pair<UiText, ResultEpisode>>) {
when (data) {
is ResourceSome.Success -> {
data.value.let { (text, ep) ->
result_play_movie.setText(text)
result_play_movie?.setOnClickListener {
viewModel.handleAction(
activity,
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
)
}
result_play_movie?.setOnLongClickListener {
viewModel.handleAction(
activity,
EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep)
)
return@setOnLongClickListener true
}
main {
val file =
ioWorkSafe {
context?.let {
VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
it,
ep.id
)
}
}
downloadButton?.dispose()
downloadButton = EasyDownloadButton()
downloadButton?.setUpMoreButton(
file?.fileLength,
file?.totalBytes,
result_movie_progress_downloaded ?: return@main,
result_movie_download_icon ?: return@main,
result_movie_download_text ?: return@main,
result_movie_download_text_precentage ?: return@main,
result_download_movie ?: return@main,
true,
VideoDownloadHelper.DownloadEpisodeCached(
ep.name,
ep.poster,
0,
null,
ep.id,
ep.id,
null,
null,
System.currentTimeMillis(),
)
) { click ->
when (click.action) {
DOWNLOAD_ACTION_DOWNLOAD -> {
viewModel.handleAction(
activity,
EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, ep)
)
}
else -> handleDownloadClick(activity, click)
}
}
result_movie_progress_downloaded_holder?.isVisible = true
}
}
}
else -> {
result_movie_progress_downloaded_holder?.isVisible = false
result_play_movie?.isVisible = false
}
}
}
open fun updateEpisodes(episodes: ResourceSome<List<ResultEpisode>>) {
when (episodes) {
is ResourceSome.None -> {
result_episode_loading?.isVisible = false
result_episodes?.isVisible = false
}
is ResourceSome.Loading -> {
result_episode_loading?.isVisible = true
result_episodes?.isVisible = false
}
is ResourceSome.Success -> {
result_episodes?.isVisible = true
result_episode_loading?.isVisible = false
/*
* 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
*/
// Do not use this.isTv, that is the player
val isTv = isTvSettings()
val hasEpisodes =
!(result_episodes?.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty()
if (isTv && hasEpisodes) {
// Make it impossible to focus anywhere else!
temporary_no_focus?.isFocusable = true
temporary_no_focus?.requestFocus()
}
(result_episodes?.adapter as? EpisodeAdapter)?.updateList(episodes.value)
if (isTv && hasEpisodes) main {
delay(500)
temporary_no_focus?.isFocusable = false
// This might make some people sad as it changes the focus when leaving an episode :(
result_episodes?.requestFocus()
}
}
}
}
}*/
data class StoredData(
val url: String?,
val url: String,
val apiName: String,
val showFillers: Boolean,
val dubStatus: DubStatus,
val start: AutoResume?,
val playerAction: Int
val playerAction: Int,
val restart : Boolean,
)
private fun getStoredData(context: Context): StoredData? {
fun Fragment.getStoredData(): StoredData? {
val context = this.context ?: this.activity ?: return null
val settingsManager = PreferenceManager.getDefaultSharedPreferences(context)
val url = arguments?.getString(URL_BUNDLE)
val url = arguments?.getString(URL_BUNDLE) ?: return null
val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return null
val showFillers =
settingsManager.getBoolean(context.getString(R.string.show_fillers_key), false)
@ -496,6 +229,11 @@ open class ResultFragment : ResultTrailerPlayer() {
val playerAction = getPlayerAction(context)
val restart = arguments?.getBoolean(RESTART_BUNDLE) ?: false
if (restart) {
arguments?.putBoolean(RESTART_BUNDLE, false)
}
val start = startAction?.let { action ->
val startValue = arguments?.getInt(START_VALUE_BUNDLE)
val resumeEpisode = arguments?.getInt(EPISODE_BUNDLE)
@ -510,10 +248,10 @@ open class ResultFragment : ResultTrailerPlayer() {
season = resumeSeason
)
}
return StoredData(url, apiName, showFillers, dubStatus, start, playerAction)
return StoredData(url, apiName, showFillers, dubStatus, start, playerAction, restart)
}
private fun reloadViewModel(forceReload: Boolean) {
/*private fun reloadViewModel(forceReload: Boolean) {
if (!viewModel.hasLoaded() || forceReload) {
val storedData = getStoredData(activity ?: context ?: return) ?: return
@ -532,26 +270,6 @@ open class ResultFragment : ResultTrailerPlayer() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
result_cast_items?.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
}
result_cast_items?.adapter = ActorAdaptor()
updateUIListener = ::updateUI
@ -565,7 +283,6 @@ open class ResultFragment : ResultTrailerPlayer() {
context?.updateHasTrailers()
activity?.loadCache()
activity?.fixPaddingStatusbar(result_top_bar)
//activity?.fixPaddingStatusbar(result_barstatus)
/* val backParameter = result_back.layoutParams as FrameLayout.LayoutParams
@ -582,533 +299,13 @@ open class ResultFragment : ResultTrailerPlayer() {
val storedData = (activity ?: context)?.let {
getStoredData(it)
}
syncModel.addFromUrl(storedData?.url)
val api = getApiFromNameNull(storedData?.apiName)
result_episodes?.adapter =
EpisodeAdapter(
api?.hasDownloadSupport == true,
{ episodeClick ->
viewModel.handleAction(activity, episodeClick)
},
{ downloadClickEvent ->
handleDownloadClick(activity, downloadClickEvent)
}
)
observe(viewModel.episodeSynopsis) { description ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(description.html())
.setTitle(R.string.synopsis)
.setOnDismissListener {
viewModel.releaseEpisodeSynopsis()
}
.show()
}
}
observe(viewModel.watchStatus) { watchType ->
result_bookmark_button?.text = getString(watchType.stringRes)
result_bookmark_fab?.text = getString(watchType.stringRes)
if (watchType == WatchType.NONE) {
result_bookmark_fab?.context?.colorFromAttribute(R.attr.white)
} else {
result_bookmark_fab?.context?.colorFromAttribute(R.attr.colorPrimary)
}?.let {
val colorState = ColorStateList.valueOf(it)
result_bookmark_fab?.iconTint = colorState
result_bookmark_fab?.setTextColor(colorState)
}
result_bookmark_fab?.setOnClickListener { fab ->
activity?.showBottomDialog(
WatchType.values().map { fab.context.getString(it.stringRes) }.toList(),
watchType.ordinal,
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
}
}
result_bookmark_button?.setOnClickListener { fab ->
activity?.showBottomDialog(
WatchType.values().map { fab.context.getString(it.stringRes) }.toList(),
watchType.ordinal,
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
viewModel.updateWatchStatus(WatchType.values()[it])
}
}
}
// This is to band-aid FireTV navigation
val isTv = isTvSettings()
result_season_button?.isFocusableInTouchMode = isTv
result_episode_select?.isFocusableInTouchMode = isTv
result_dub_select?.isFocusableInTouchMode = isTv
context?.let { ctx ->
val arrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
/*
-1 -> None
0 -> Watching
1 -> Completed
2 -> OnHold
3 -> Dropped
4 -> PlanToWatch
5 -> ReWatching
*/
val items = listOf(
R.string.none,
R.string.type_watching,
R.string.type_completed,
R.string.type_on_hold,
R.string.type_dropped,
R.string.type_plan_to_watch,
R.string.type_re_watching
).map { ctx.getString(it) }
arrayAdapter.addAll(items)
result_sync_check?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
result_sync_check?.adapter = arrayAdapter
UIHelper.setListViewHeightBasedOnItems(result_sync_check)
result_sync_check?.setOnItemClickListener { _, _, which, _ ->
syncModel.setStatus(which - 1)
}
result_sync_rating?.addOnChangeListener { _, value, _ ->
syncModel.setScore(value.toInt())
}
result_sync_add_episode?.setOnClickListener {
syncModel.setEpisodesDelta(1)
}
result_sync_sub_episode?.setOnClickListener {
syncModel.setEpisodesDelta(-1)
}
result_sync_current_episodes?.doOnTextChanged { text, _, before, count ->
if (count == before) return@doOnTextChanged
text?.toString()?.toIntOrNull()?.let { ep ->
syncModel.setEpisodes(ep)
}
}
}
observe(syncModel.synced) { list ->
result_sync_names?.text =
list.filter { it.isSynced && it.hasAccount }.joinToString { it.name }
val newList = list.filter { it.isSynced && it.hasAccount }
result_mini_sync?.isVisible = newList.isNotEmpty()
(result_mini_sync?.adapter as? ImageAdapter)?.updateList(newList.mapNotNull { it.icon })
}
var currentSyncProgress = 0
fun setSyncMaxEpisodes(totalEpisodes: Int?) {
result_sync_episodes?.max = (totalEpisodes ?: 0) * 1000
normalSafeApiCall {
val ctx = result_sync_max_episodes?.context
result_sync_max_episodes?.text =
totalEpisodes?.let { episodes ->
ctx?.getString(R.string.sync_total_episodes_some)?.format(episodes)
} ?: run {
ctx?.getString(R.string.sync_total_episodes_none)
}
}
}
observe(syncModel.metadata) { meta ->
when (meta) {
is Resource.Success -> {
val d = meta.value
result_sync_episodes?.progress = currentSyncProgress * 1000
setSyncMaxEpisodes(d.totalEpisodes)
viewModel.setMeta(d, syncModel.getSyncs())
}
is Resource.Loading -> {
result_sync_max_episodes?.text =
result_sync_max_episodes?.context?.getString(R.string.sync_total_episodes_none)
}
else -> {}
}
}
observe(syncModel.userData) { status ->
var closed = false
when (status) {
is Resource.Failure -> {
result_sync_loading_shimmer?.stopShimmer()
result_sync_loading_shimmer?.isVisible = false
result_sync_holder?.isVisible = false
closed = true
}
is Resource.Loading -> {
result_sync_loading_shimmer?.startShimmer()
result_sync_loading_shimmer?.isVisible = true
result_sync_holder?.isVisible = false
}
is Resource.Success -> {
result_sync_loading_shimmer?.stopShimmer()
result_sync_loading_shimmer?.isVisible = false
result_sync_holder?.isVisible = true
val d = status.value
result_sync_rating?.value = d.score?.toFloat() ?: 0.0f
result_sync_check?.setItemChecked(d.status + 1, true)
val watchedEpisodes = d.watchedEpisodes ?: 0
currentSyncProgress = watchedEpisodes
d.maxEpisodes?.let {
// don't directly call it because we don't want to override metadata observe
setSyncMaxEpisodes(it)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result_sync_episodes?.setProgress(watchedEpisodes * 1000, true)
} else {
result_sync_episodes?.progress = watchedEpisodes * 1000
}
result_sync_current_episodes?.text =
Editable.Factory.getInstance()?.newEditable(watchedEpisodes.toString())
normalSafeApiCall { // format might fail
context?.getString(R.string.sync_score_format)?.format(d.score ?: 0)?.let {
result_sync_score_text?.text = it
}
}
}
null -> {
closed = false
}
}
result_overlapping_panels?.setStartPanelLockState(if (closed) OverlappingPanelsLayout.LockState.CLOSE else OverlappingPanelsLayout.LockState.UNLOCKED)
}
observe(viewModel.resumeWatching) { resume ->
when (resume) {
is Some.Success -> {
result_resume_parent?.isVisible = true
val value = resume.value
value.progress?.let { progress ->
result_resume_series_title?.apply {
isVisible = !value.isMovie
text =
if (value.isMovie) null else activity?.getNameFull(
value.result.name,
value.result.episode,
value.result.season
)
}
result_resume_series_progress_text.setText(progress.progressLeft)
result_resume_series_progress?.apply {
isVisible = true
this.max = progress.maxProgress
this.progress = progress.progress
}
result_resume_progress_holder?.isVisible = true
} ?: run {
result_resume_progress_holder?.isVisible = false
result_resume_series_progress?.isVisible = false
result_resume_series_title?.isVisible = false
result_resume_series_progress_text?.isVisible = false
}
result_resume_series_button?.isVisible = !value.isMovie
result_resume_series_button_play?.isVisible = !value.isMovie
val click = View.OnClickListener {
viewModel.handleAction(
activity,
EpisodeClickEvent(
storedData?.playerAction ?: ACTION_PLAY_EPISODE_IN_PLAYER,
value.result
)
)
}
result_resume_series_button?.setOnClickListener(click)
result_resume_series_button_play?.setOnClickListener(click)
}
is Some.None -> {
result_resume_parent?.isVisible = false
}
}
}
observe(viewModel.episodes) { episodes ->
updateEpisodes(episodes)
}
result_cast_items?.setOnFocusChangeListener { _, hasFocus ->
// Always escape focus
if (hasFocus) result_bookmark_button?.requestFocus()
}
result_sync_set_score?.setOnClickListener {
syncModel.publishUserData()
}
observe(viewModel.trailers) { trailers ->
setTrailers(trailers.flatMap { it.mirros }) // I dont care about subtitles yet!
}
observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null)
}
observe(viewModel.movie) { data ->
updateMovie(data)
}
observe(viewModel.page) { data ->
if (data == null) return@observe
when (data) {
is Resource.Success -> {
val d = data.value
updateVisStatus(2)
result_vpn.setText(d.vpnText)
result_info.setText(d.metaText)
result_no_episodes.setText(d.noEpisodesFoundText)
result_title.setText(d.titleText)
result_meta_site.setText(d.apiName)
result_meta_type.setText(d.typeText)
result_meta_year.setText(d.yearText)
result_meta_duration.setText(d.durationText)
result_meta_rating.setText(d.ratingText)
result_cast_text.setText(d.actorsText)
result_next_airing.setText(d.nextAiringEpisode)
result_next_airing_time.setText(d.nextAiringDate)
result_poster.setImage(d.posterImage)
result_poster_background.setImage(d.posterBackgroundImage)
//result_trailer_thumbnail.setImage(d.posterBackgroundImage, fadeIn = false)
if (d.posterImage != null && !isTrueTvSettings())
result_poster_holder?.setOnClickListener {
try {
context?.let { ctx ->
runBlocking {
val sourceBuilder = AlertDialog.Builder(ctx)
sourceBuilder.setView(R.layout.result_poster)
val sourceDialog = sourceBuilder.create()
sourceDialog.show()
sourceDialog.findViewById<ImageView?>(R.id.imgPoster)
?.apply {
setImage(d.posterImage)
setOnClickListener {
sourceDialog.dismissSafe()
}
}
}
}
} catch (e: Exception) {
logError(e)
}
}
result_cast_items?.isVisible = d.actors != null
(result_cast_items?.adapter as? ActorAdaptor)?.apply {
updateList(d.actors ?: emptyList())
}
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
result_subscribe?.isVisible = isSubscribed != null
if (isSubscribed == null) return@observeNullable
val drawable = if (isSubscribed) {
R.drawable.ic_baseline_notifications_active_24
} else {
R.drawable.baseline_notifications_none_24
}
result_subscribe?.setImageResource(drawable)
}
result_subscribe?.setOnClickListener {
val isSubscribed =
viewModel.toggleSubscriptionStatus() ?: return@setOnClickListener
val message = if (isSubscribed) {
// Kinda icky to have this here, but it works.
SubscriptionWorkManager.enqueuePeriodicWork(context)
R.string.subscription_new
} else {
R.string.subscription_deleted
}
val name = (viewModel.page.value as? Resource.Success)?.value?.title
?: txt(R.string.no_data).asStringNull(context) ?: ""
showToast(activity, txt(message, name), Toast.LENGTH_SHORT)
}
result_open_in_browser?.isVisible = d.url.startsWith("http")
result_open_in_browser?.setOnClickListener {
val i = Intent(ACTION_VIEW)
i.data = Uri.parse(d.url)
try {
startActivity(i)
} catch (e: Exception) {
logError(e)
}
}
result_search?.setOnClickListener {
QuickSearchFragment.pushSearch(activity, d.title)
}
result_share?.setOnClickListener {
try {
val i = Intent(ACTION_SEND)
i.type = "text/plain"
i.putExtra(EXTRA_SUBJECT, d.title)
i.putExtra(EXTRA_TEXT, d.url)
startActivity(createChooser(i, d.title))
} catch (e: Exception) {
logError(e)
}
}
if (syncModel.addSyncs(d.syncData)) {
syncModel.updateMetaAndUser()
syncModel.updateSynced()
} else {
syncModel.addFromUrl(d.url)
}
result_description.setTextHtml(d.plotText)
if (this !is ResultFragmentTv) // dont want this clickable on tv layout
result_description?.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()
}
}
d.comingSoon.let { soon ->
result_coming_soon?.isVisible = soon
result_data_holder?.isGone = soon
}
val tags = d.tags
result_tag_holder?.isVisible = tags.isNotEmpty()
result_tag?.apply {
removeAllViews()
tags.forEach { tag ->
val chip = Chip(context)
val chipDrawable = ChipDrawable.createFromAttributes(
context,
null,
0,
R.style.ChipFilled
)
chip.setChipDrawable(chipDrawable)
chip.text = tag
chip.isChecked = false
chip.isCheckable = false
chip.isFocusable = false
chip.isClickable = false
chip.setTextColor(context.colorFromAttribute(R.attr.textColor))
addView(chip)
}
}
// if (tags.isNotEmpty()) {
//result_tag_holder?.visibility = VISIBLE
//val isOnTv = isTrueTvSettings()
/*for ((index, tag) in tags.withIndex()) {
val viewBtt = layoutInflater.inflate(R.layout.result_tag, null)
val btt = viewBtt.findViewById<MaterialButton>(R.id.result_tag_card)
btt.text = tag
btt.isFocusable = !isOnTv
btt.isClickable = !isOnTv
result_tag?.addView(viewBtt, index)
}*/
//}
}
is Resource.Failure -> {
result_error_text.text = storedData?.url?.plus("\n") + data.errorString
updateVisStatus(1)
}
is Resource.Loading -> {
updateVisStatus(0)
}
}
}
context?.let { ctx ->
//result_bookmark_button?.isVisible = ctx.isTvSettings()
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
Kitsu.isEnabled =
settingsManager.getBoolean(ctx.getString(R.string.show_kitsu_posters_key), true)
if (storedData?.url != null) {
result_reload_connectionerror.setOnClickListener {
viewModel.load(
activity,
storedData.url,
storedData.apiName,
storedData.showFillers,
storedData.dubStatus,
storedData.start
)
}
result_reload_connection_open_in_browser?.setOnClickListener {
val i = Intent(ACTION_VIEW)
i.data = Uri.parse(storedData.url)
try {
startActivity(i)
} catch (e: Exception) {
logError(e)
}
}
result_open_in_browser?.isVisible = storedData.url.startsWith("http")
result_open_in_browser?.setOnClickListener {
val i = Intent(ACTION_VIEW)
i.data = Uri.parse(storedData.url)
try {
startActivity(i)
} catch (e: Exception) {
logError(e)
}
}
// bloats the navigation on tv
if (!isTrueTvSettings()) {
result_meta_site?.setOnClickListener {
it.context?.openBrowser(storedData.url)
}
result_meta_site?.isFocusable = true
} else {
result_meta_site?.isFocusable = false
}
if (restart || !viewModel.hasLoaded()) {
//viewModel.clear()
viewModel.load(
@ -1121,6 +318,5 @@ open class ResultFragment : ResultTrailerPlayer() {
)
}
}
}
}
}*/
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,8 +20,11 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding
import com.lagradost.cloudstream3.databinding.AddSiteInputBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient
@ -38,8 +41,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
import com.lagradost.cloudstream3.utils.VideoDownloadManager
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
import kotlinx.android.synthetic.main.add_remove_sites.*
import kotlinx.android.synthetic.main.add_site_input.*
import java.io.File
fun getCurrentLocale(context: Context): String {
@ -188,7 +189,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
fun showAdd() {
val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name }
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" },
-1,
@ -197,21 +198,23 @@ class SettingsGeneral : PreferenceFragmentCompat() {
{}) { selection ->
val provider = providers.getOrNull(selection) ?: return@showDialog
val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(layoutInflater,null,false)
val builder =
AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom)
.setView(R.layout.add_site_input)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
dialog.text2?.text = provider.name
dialog.apply_btt?.setOnClickListener {
val name = dialog.site_name_input?.text?.toString()
val url = dialog.site_url_input?.text?.toString()
val lang = dialog.site_lang_input?.text?.toString()
binding.text2.text = provider.name
binding.applyBtt.setOnClickListener {
val name = binding.siteNameInput.text?.toString()
val url = binding.siteUrlInput.text?.toString()
val lang = binding.siteLangInput.text?.toString()
val realLang = if (lang.isNullOrBlank()) provider.lang else lang
if (url.isNullOrBlank() || name.isNullOrBlank() || realLang.length != 2) {
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT)
return@setOnClickListener
}
@ -219,10 +222,12 @@ class SettingsGeneral : PreferenceFragmentCompat() {
val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang)
current.add(newSite)
setKey(USER_PROVIDER_API, current.toTypedArray())
// reload apis
MainActivity.afterPluginsLoadedEvent.invoke(false)
dialog.dismissSafe(activity)
}
dialog.cancel_btt?.setOnClickListener {
binding.cancelBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
}
@ -242,18 +247,19 @@ class SettingsGeneral : PreferenceFragmentCompat() {
}
fun showAddOrDelete() {
val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate(layoutInflater,null,false)
val builder =
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
.setView(R.layout.add_remove_sites)
.setView(binding.root)
val dialog = builder.create()
dialog.show()
dialog.add_site?.setOnClickListener {
binding.addSite.setOnClickListener {
showAdd()
dialog.dismissSafe(activity)
}
dialog.remove_site?.setOnClickListener {
binding.removeSite.setOnClickListener {
showDelete()
dialog.dismissSafe(activity)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.utils.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event
@ -37,8 +38,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import kotlinx.android.synthetic.main.subtitle_settings.*
import kotlinx.android.synthetic.main.toast.view.*
import java.io.File
const val SUBTITLE_KEY = "subtitle_settings"
@ -184,10 +183,10 @@ class SubtitlesFragment : Fragment() {
}
private fun Context.updateState() {
subtitle_text?.setStyle(fromSaveToStyle(state))
val text = subtitle_text.context.getString(R.string.subtitles_example_text)
binding?.subtitleText?.setStyle(fromSaveToStyle(state))
val text = getString(R.string.subtitles_example_text)
val fixedText = if (state.upperCase) text.uppercase() else text
subtitle_text?.setCues(
binding?.subtitleText?.setCues(
listOf(
Cue.Builder()
.setTextSize(
@ -213,12 +212,21 @@ class SubtitlesFragment : Fragment() {
return if (color == Color.TRANSPARENT) Color.BLACK else color
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
}
var binding: SubtitleSettingsBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.subtitle_settings, container, false)
savedInstanceState: Bundle?
): View {
val localBinding = SubtitleSettingsBinding.inflate(inflater, container, false)
binding = localBinding
return localBinding.root
//return inflater.inflate(R.layout.subtitle_settings, container, false)
}
private lateinit var state: SaveCaptionStyle
@ -234,11 +242,11 @@ class SubtitlesFragment : Fragment() {
hide = arguments?.getBoolean("hide") ?: true
onColorSelectedEvent += ::onColorSelected
onDialogDismissedEvent += ::onDialogDismissed
subs_import_text?.text = getString(R.string.subs_import_text).format(
binding?.subsImportText?.text = getString(R.string.subs_import_text).format(
context?.getExternalFilesDir(null)?.absolutePath.toString() + "/Fonts"
)
context?.fixPaddingStatusbar(subs_root)
fixPaddingStatusbar(binding?.subsRoot)
state = getCurrentSavedStyle()
context?.updateState()
@ -264,23 +272,23 @@ class SubtitlesFragment : Fragment() {
this.setOnLongClickListener {
it.context.setColor(id, null)
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
}
subs_text_color.setup(0)
subs_outline_color.setup(1)
subs_background_color.setup(2)
subs_window_color.setup(3)
binding?.apply {
subsTextColor.setup(0)
subsOutlineColor.setup(1)
subsBackgroundColor.setup(2)
subsWindowColor.setup(3)
val dismissCallback = {
if (hide)
activity?.hideSystemUI()
}
subs_subtitle_elevation.setFocusableInTv()
subs_subtitle_elevation.setOnClickListener { textView ->
subsSubtitleElevation.setFocusableInTv()
subsSubtitleElevation.setOnClickListener { textView ->
val suffix = "dp"
val elevationTypes = listOf(
Pair(0, textView.context.getString(R.string.none)),
@ -311,15 +319,15 @@ class SubtitlesFragment : Fragment() {
}
}
subs_subtitle_elevation.setOnLongClickListener {
subsSubtitleElevation.setOnLongClickListener {
state.elevation = DEF_SUBS_ELEVATION
it.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subs_edge_type.setFocusableInTv()
subs_edge_type.setOnClickListener { textView ->
subsEdgeType.setFocusableInTv()
subsEdgeType.setOnClickListener { textView ->
val edgeTypes = listOf(
Pair(
CaptionStyleCompat.EDGE_TYPE_NONE,
@ -356,15 +364,15 @@ class SubtitlesFragment : Fragment() {
}
}
subs_edge_type.setOnLongClickListener {
subsEdgeType.setOnLongClickListener {
state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE
it.context.updateState()
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subs_font_size.setFocusableInTv()
subs_font_size.setOnClickListener { textView ->
subsFontSize.setFocusableInTv()
subsFontSize.setOnClickListener { textView ->
val suffix = "sp"
val fontSizes = listOf(
Pair(null, textView.context.getString(R.string.normal)),
@ -415,22 +423,22 @@ class SubtitlesFragment : Fragment() {
}
}
subtitles_remove_bloat?.isChecked = state.removeBloat
subtitles_remove_bloat?.setOnCheckedChangeListener { _, b ->
subtitlesRemoveBloat.isChecked = state.removeBloat
subtitlesRemoveBloat.setOnCheckedChangeListener { _, b ->
state.removeBloat = b
}
subtitles_uppercase?.isChecked = state.upperCase
subtitles_uppercase?.setOnCheckedChangeListener { _, b ->
subtitlesUppercase.isChecked = state.upperCase
subtitlesUppercase.setOnCheckedChangeListener { _, b ->
state.upperCase = b
context?.updateState()
}
subtitles_remove_captions?.isChecked = state.removeCaptions
subtitles_remove_captions?.setOnCheckedChangeListener { _, b ->
subtitlesRemoveCaptions.isChecked = state.removeCaptions
subtitlesRemoveCaptions.setOnCheckedChangeListener { _, b ->
state.removeCaptions = b
}
subs_font_size.setOnLongClickListener { _ ->
subsFontSize.setOnLongClickListener { _ ->
state.fixedTextSize = null
//textView.context.updateState() // font size not changed
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
@ -439,12 +447,12 @@ class SubtitlesFragment : Fragment() {
//Fetch current value from preference
context?.let { ctx ->
subtitles_filter_sub_lang?.isChecked =
subtitlesFilterSubLang.isChecked =
PreferenceManager.getDefaultSharedPreferences(ctx)
.getBoolean(getString(R.string.filter_sub_lang_key), false)
}
subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b ->
subtitlesFilterSubLang.setOnCheckedChangeListener { _, b ->
context?.let { ctx ->
PreferenceManager.getDefaultSharedPreferences(ctx)
.edit()
@ -453,8 +461,8 @@ class SubtitlesFragment : Fragment() {
}
}
subs_font.setFocusableInTv()
subs_font.setOnClickListener { textView ->
subsFont.setFocusableInTv()
subsFont.setOnClickListener { textView ->
val fontTypes = listOf(
Pair(null, textView.context.getString(R.string.normal)),
Pair(R.font.trebuchet_ms, "Trebuchet MS"),
@ -501,7 +509,7 @@ class SubtitlesFragment : Fragment() {
}
}
subs_font.setOnLongClickListener { textView ->
subsFont.setOnLongClickListener { textView ->
state.typeface = null
state.typefaceFilePath = null
textView.context.updateState()
@ -509,8 +517,8 @@ class SubtitlesFragment : Fragment() {
return@setOnLongClickListener true
}
subs_auto_select_language.setFocusableInTv()
subs_auto_select_language.setOnClickListener { textView ->
subsAutoSelectLanguage.setFocusableInTv()
subsAutoSelectLanguage.setOnClickListener { textView ->
val langMap = arrayListOf(
SubtitleHelper.Language639(
textView.context.getString(R.string.none),
@ -536,14 +544,14 @@ class SubtitlesFragment : Fragment() {
}
}
subs_auto_select_language.setOnLongClickListener {
subsAutoSelectLanguage.setOnLongClickListener {
setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
subs_download_languages.setFocusableInTv()
subs_download_languages.setOnClickListener { textView ->
subsDownloadLanguages.setFocusableInTv()
subsDownloadLanguages.setOnClickListener { textView ->
val langMap = SubtitleHelper.languages
val lang639_1 = langMap.map { it.ISO_639_1 }
val keys = getDownloadSubsLanguageISO639_1()
@ -559,18 +567,18 @@ class SubtitlesFragment : Fragment() {
}
}
subs_download_languages.setOnLongClickListener {
subsDownloadLanguages.setOnLongClickListener {
setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
return@setOnLongClickListener true
}
cancel_btt.setOnClickListener {
cancelBtt.setOnClickListener {
activity?.popCurrentPage()
}
apply_btt.setOnClickListener {
applyBtt.setOnClickListener {
it.context.saveStyle(state)
applyStyleEvent.invoke(state)
it.context.fromSaveToStyle(state)
@ -578,3 +586,4 @@ class SubtitlesFragment : Fragment() {
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import java.util.concurrent.TimeUnit
object SyncUtil {
private val regexs = listOf(
Regex("""(9anime)\.(?:to|center|id)/watch/(?:.*?)\.([^/?]*)"""),
Regex("""(9anime)\.(?:to|center|id)/watch/.*?\.([^/?]*)"""),
Regex("""(gogoanime|gogoanimes)\..*?/category/([^/?]*)"""),
Regex("""(twist\.moe)/a/([^/?]*)"""),
)
@ -44,6 +44,13 @@ object SyncUtil {
matchList[site]?.let { realSite ->
getIdsFromSlug(slug, realSite)?.let {
return it
} ?: kotlin.run {
if (slug.endsWith("-dub")) {
println("testing non -dub slug $slug")
getIdsFromSlug(slug.removeSuffix("-dub"), realSite)?.let {
return it
}
}
}
}
}
@ -89,10 +96,12 @@ object SyncUtil {
.mapNotNull { it.url }.toMutableList()
if (type == "anilist") { // TODO MAKE BETTER
synchronized(apis) {
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id")
}
}
}
return current
}

View File

@ -211,7 +211,7 @@ object TestingUtils {
fun getDeferredProviderTests(
scope: CoroutineScope,
providers: List<MainAPI>,
providers: Array<MainAPI>,
logger: (String) -> Unit,
callback: (MainAPI, TestResultProvider) -> Unit
) {

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