mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
a0c112ad3c
201 changed files with 9890 additions and 6807 deletions
4
.github/workflows/build_to_archive.yml
vendored
4
.github/workflows/build_to_archive.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/generate_dokka.yml
vendored
4
.github/workflows/generate_dokka.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/prerelease.yml
vendored
4
.github/workflows/prerelease.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/pull_request.yml
vendored
4
.github/workflows/pull_request.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
</project>
|
|
@ -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$" />
|
||||
|
|
|
@ -7,7 +7,6 @@ plugins {
|
|||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("kotlin-kapt")
|
||||
id("kotlin-android-extensions")
|
||||
id("org.jetbrains.dokka")
|
||||
}
|
||||
|
||||
|
@ -28,6 +27,11 @@ android {
|
|||
testOptions {
|
||||
unitTests.isReturnDefaultValues = true
|
||||
}
|
||||
|
||||
viewBinding {
|
||||
enable = true
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("prerelease") {
|
||||
if (prereleaseStoreFile != null) {
|
||||
|
@ -48,7 +52,7 @@ android {
|
|||
targetSdk = 33
|
||||
|
||||
versionCode = 59
|
||||
versionName = "4.0.1"
|
||||
versionName = "4.1.1"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
|
||||
|
@ -143,6 +147,7 @@ dependencies {
|
|||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||
androidTestImplementation("androidx.test:core")
|
||||
|
||||
//implementation("io.karn:khttp-android:0.1.2") //okhttp instead
|
||||
// implementation("org.jsoup:jsoup:1.13.1")
|
||||
|
@ -215,7 +220,7 @@ dependencies {
|
|||
|
||||
implementation("com.github.discord:OverlappingPanels:0.1.3")
|
||||
// debugImplementation because LeakCanary should only run in debug builds.
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
|
||||
//debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
|
||||
|
||||
// for shimmer when loading
|
||||
implementation("com.facebook.shimmer:shimmer:0.5.0")
|
||||
|
@ -229,13 +234,13 @@ dependencies {
|
|||
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
||||
|
||||
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L204
|
||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:master-SNAPSHOT")
|
||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:8495ad619e")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||
|
||||
// Library/extensions searching with Levenshtein distance
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||
|
||||
// color pallette for images -> colors
|
||||
// color palette for images -> colors
|
||||
implementation("androidx.palette:palette-ktx:1.0.0")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.content.res.Resources
|
|||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.View.NO_ID
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
|
@ -27,6 +28,7 @@ import com.lagradost.cloudstream3.ui.player.PlayerEventType
|
|||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.result.UiText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
|
@ -34,9 +36,18 @@ import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.shouldShowPIPMode
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
|
||||
object CommonActivity {
|
||||
|
||||
private var _activity: WeakReference<Activity>? = null
|
||||
var activity
|
||||
get() = _activity?.get()
|
||||
private set(value) {
|
||||
_activity = WeakReference(value)
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun Activity?.getCastSession(): CastSession? {
|
||||
return (this as MainActivity?)?.mSessionManager?.currentCastSession
|
||||
|
@ -56,6 +67,30 @@ object CommonActivity {
|
|||
|
||||
var currentToast: Toast? = null
|
||||
|
||||
fun showToast(@StringRes message: Int, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
act.runOnUiThread {
|
||||
showToast(act, act.getString(message), duration)
|
||||
}
|
||||
}
|
||||
|
||||
fun showToast(message: String?, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
act.runOnUiThread {
|
||||
showToast(act, message, duration)
|
||||
}
|
||||
}
|
||||
|
||||
fun showToast(message: UiText?, duration: Int? = null) {
|
||||
val act = activity ?: return
|
||||
if (message == null) return
|
||||
act.runOnUiThread {
|
||||
showToast(act, message.asString(act), duration)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainThread
|
||||
fun showToast(act: Activity?, text: UiText, duration: Int) {
|
||||
if (act == null) return
|
||||
text.asStringNull(act)?.let {
|
||||
|
@ -140,6 +175,7 @@ object CommonActivity {
|
|||
|
||||
fun init(act: ComponentActivity?) {
|
||||
if (act == null) return
|
||||
activity = act
|
||||
//https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission
|
||||
//https://developer.android.com/guide/topics/ui/picture-in-picture
|
||||
canShowPipMode =
|
||||
|
@ -260,27 +296,58 @@ object CommonActivity {
|
|||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
||||
}
|
||||
|
||||
/** because we want closes find, aka when multiple have the same id, we go to parent
|
||||
until the correct one is found */
|
||||
private fun localLook(from: View, id: Int): View? {
|
||||
if (id == NO_ID) return null
|
||||
var currentLook: View = from
|
||||
while (true) {
|
||||
currentLook.findViewById<View?>(id)?.let { return it }
|
||||
currentLook = (currentLook.parent as? View) ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
/*var currentLook: View = view
|
||||
while (true) {
|
||||
val tmpNext = currentLook.findViewById<View?>(nextId)
|
||||
if (tmpNext != null) {
|
||||
next = tmpNext
|
||||
break
|
||||
}
|
||||
currentLook = currentLook.parent as? View ?: break
|
||||
}*/
|
||||
|
||||
/** recursively looks for a next focus up to a depth of 10,
|
||||
* this is used to override the normal shit focus system
|
||||
* because this application has a lot of invisible views that messes with some tv devices*/
|
||||
private fun getNextFocus(
|
||||
act: Activity?,
|
||||
view: View?,
|
||||
direction: FocusDirection,
|
||||
depth: Int = 0
|
||||
): Int? {
|
||||
): View? {
|
||||
// if input is invalid let android decide + depth test to not crash if loop is found
|
||||
if (view == null || depth >= 10 || act == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val nextId = when (direction) {
|
||||
FocusDirection.Left -> {
|
||||
view.nextFocusLeftId
|
||||
var nextId = when (direction) {
|
||||
FocusDirection.Start -> {
|
||||
if (view.isRtl())
|
||||
view.nextFocusRightId
|
||||
else
|
||||
view.nextFocusLeftId
|
||||
}
|
||||
|
||||
FocusDirection.Up -> {
|
||||
view.nextFocusUpId
|
||||
}
|
||||
|
||||
FocusDirection.Right -> {
|
||||
view.nextFocusRightId
|
||||
FocusDirection.End -> {
|
||||
if (view.isRtl())
|
||||
view.nextFocusLeftId
|
||||
else
|
||||
view.nextFocusRightId
|
||||
}
|
||||
|
||||
FocusDirection.Down -> {
|
||||
|
@ -288,27 +355,35 @@ object CommonActivity {
|
|||
}
|
||||
}
|
||||
|
||||
return if (nextId != -1) {
|
||||
val next = act.findViewById<View?>(nextId)
|
||||
//println("NAME: ${next.accessibilityClassName} | ${next?.isShown}" )
|
||||
|
||||
if (next?.isShown == false) {
|
||||
getNextFocus(act, next, direction, depth + 1)
|
||||
} else {
|
||||
if (depth == 0) {
|
||||
null
|
||||
} else {
|
||||
nextId
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
if (nextId == NO_ID) {
|
||||
// if not specified then use forward id
|
||||
nextId = view.nextFocusForwardId
|
||||
// if view is still not found to next focus then return and let android decide
|
||||
if (nextId == NO_ID) return null
|
||||
}
|
||||
|
||||
var next = act.findViewById<View?>(nextId) ?: return null
|
||||
|
||||
next = localLook(view, nextId) ?: next
|
||||
|
||||
var currentLook: View = view
|
||||
while (currentLook.findViewById<View?>(nextId)?.also { next = it } == null) {
|
||||
currentLook = (currentLook.parent as? View) ?: break
|
||||
}
|
||||
|
||||
// if cant focus but visible then break and let android decide
|
||||
if (!next.isFocusable && next.isShown) return null
|
||||
|
||||
// if not shown then continue because we will "skip" over views to get to a replacement
|
||||
if (!next.isShown) return getNextFocus(act, next, direction, depth + 1)
|
||||
|
||||
// nothing wrong with the view found, return it
|
||||
return next
|
||||
}
|
||||
|
||||
enum class FocusDirection {
|
||||
Left,
|
||||
Right,
|
||||
private enum class FocusDirection {
|
||||
Start,
|
||||
End,
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
@ -407,67 +482,64 @@ object CommonActivity {
|
|||
//}
|
||||
}
|
||||
|
||||
/** overrides focus and custom key events */
|
||||
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
||||
if (act == null) return null
|
||||
val currentFocus = act.currentFocus
|
||||
|
||||
event?.keyCode?.let { keyCode ->
|
||||
when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
if (act.currentFocus != null) {
|
||||
val next = when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Left
|
||||
)
|
||||
if (currentFocus == null || event.action != KeyEvent.ACTION_DOWN) return@let
|
||||
val nextView = when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Start
|
||||
)
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Right
|
||||
)
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.End
|
||||
)
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Up
|
||||
)
|
||||
KeyEvent.KEYCODE_DPAD_UP -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Up
|
||||
)
|
||||
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
||||
act,
|
||||
act.currentFocus,
|
||||
FocusDirection.Down
|
||||
)
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> getNextFocus(
|
||||
act,
|
||||
currentFocus,
|
||||
FocusDirection.Down
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (next != null && next != -1) {
|
||||
val nextView = act.findViewById<View?>(next)
|
||||
if (nextView != null) {
|
||||
nextView.requestFocus()
|
||||
keyEventListener?.invoke(Pair(event, true))
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_CENTER -> {
|
||||
if (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete) {
|
||||
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//println("Keycode: $keyCode")
|
||||
//showToast(
|
||||
// this,
|
||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||
// Toast.LENGTH_LONG
|
||||
//)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
if (nextView != null) {
|
||||
nextView.requestFocus()
|
||||
keyEventListener?.invoke(Pair(event, true))
|
||||
return true
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
|
||||
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
|
||||
) {
|
||||
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||
}
|
||||
|
||||
//println("Keycode: $keyCode")
|
||||
//showToast(
|
||||
// this,
|
||||
// "Got Keycode $keyCode | ${KeyEvent.keyCodeToString(keyCode)} \n ${event?.action}",
|
||||
// Toast.LENGTH_LONG
|
||||
//)
|
||||
|
||||
}
|
||||
|
||||
// if someone else want to override the focus then don't handle the event as it is already
|
||||
// consumed. used in video player
|
||||
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -50,8 +50,10 @@ object APIHolder {
|
|||
val allProviders = threadSafeListOf<MainAPI>()
|
||||
|
||||
fun initAll() {
|
||||
for (api in allProviders) {
|
||||
api.init()
|
||||
synchronized(allProviders) {
|
||||
for (api in allProviders) {
|
||||
api.init()
|
||||
}
|
||||
}
|
||||
apiMap = null
|
||||
}
|
||||
|
@ -64,27 +66,35 @@ object APIHolder {
|
|||
var apiMap: Map<String, Int>? = null
|
||||
|
||||
fun addPluginMapping(plugin: MainAPI) {
|
||||
apis = apis + plugin
|
||||
synchronized(apis) {
|
||||
apis = apis + plugin
|
||||
}
|
||||
initMap(true)
|
||||
}
|
||||
|
||||
fun removePluginMapping(plugin: MainAPI) {
|
||||
apis = apis.filter { it != plugin }
|
||||
synchronized(apis) {
|
||||
apis = apis.filter { it != plugin }
|
||||
}
|
||||
initMap(true)
|
||||
}
|
||||
|
||||
private fun initMap(forcedUpdate: Boolean = false) {
|
||||
if (apiMap == null || forcedUpdate)
|
||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||
synchronized(apis) {
|
||||
if (apiMap == null || forcedUpdate)
|
||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
||||
if (apiName == null) return null
|
||||
synchronized(allProviders) {
|
||||
initMap()
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
?: allProviders.firstOrNull { it.name == apiName }
|
||||
synchronized(apis) {
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
?: allProviders.firstOrNull { it.name == apiName }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,7 +225,7 @@ object APIHolder {
|
|||
val hashSet = HashSet<String>()
|
||||
val activeLangs = getApiProviderLangSettings()
|
||||
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
||||
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
|
||||
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
|
||||
.map { it.name })
|
||||
|
||||
/*val set = settingsManager.getStringSet(
|
||||
|
@ -314,8 +324,9 @@ object APIHolder {
|
|||
} ?: default
|
||||
val langs = this.getApiProviderLangSettings()
|
||||
val hasUniversal = langs.contains(AllLanguagesName)
|
||||
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
|
||||
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
||||
val allApis = synchronized(apis) {
|
||||
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
|
||||
}
|
||||
return if (currentPrefMedia.isEmpty()) {
|
||||
allApis
|
||||
} else {
|
||||
|
@ -736,6 +747,7 @@ fun fixTitle(str: String): String {
|
|||
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rhino context in a safe way as it needs to be initialized on the main thread.
|
||||
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||
|
@ -1122,7 +1134,7 @@ interface LoadResponse {
|
|||
var isTrailersEnabled = true
|
||||
|
||||
fun LoadResponse.isMovie(): Boolean {
|
||||
return this.type.isMovieType()
|
||||
return this.type.isMovieType() || this is MovieLoadResponse
|
||||
}
|
||||
|
||||
@JvmName("addActorNames")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.lagradost.cloudstream3
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -14,13 +15,17 @@ import android.view.KeyEvent
|
|||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
@ -40,6 +45,7 @@ import com.google.android.gms.cast.framework.CastContext
|
|||
import com.google.android.gms.cast.framework.Session
|
||||
import com.google.android.gms.cast.framework.SessionManager
|
||||
import com.google.android.gms.cast.framework.SessionManagerListener
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.navigationrail.NavigationRailView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -58,6 +64,11 @@ import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.CommonActivity.updateLocale
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.databinding.ActivityMainBinding
|
||||
import com.lagradost.cloudstream3.databinding.ActivityMainTvBinding
|
||||
import com.lagradost.cloudstream3.databinding.BottomResultviewPreviewBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
|
@ -69,7 +80,7 @@ import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver
|
|||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringPlayer
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringRepo
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringSearch
|
||||
|
@ -85,6 +96,7 @@ import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
|||
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
||||
import com.lagradost.cloudstream3.ui.result.setImage
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.search.SearchFragment
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
|
||||
|
@ -97,12 +109,14 @@ import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
|
|||
import com.lagradost.cloudstream3.utils.ApkInstaller
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isLtr
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isNetworkAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
|
@ -121,32 +135,19 @@ import com.lagradost.cloudstream3.utils.UIHelper.getResourceColor
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
import com.lagradost.nicehttp.Requests
|
||||
import com.lagradost.nicehttp.ResponseParser
|
||||
import kotlinx.android.synthetic.main.activity_main.cast_mini_controller_holder
|
||||
import kotlinx.android.synthetic.main.activity_main.nav_host_fragment
|
||||
import kotlinx.android.synthetic.main.activity_main.nav_rail_view
|
||||
import kotlinx.android.synthetic.main.activity_main.nav_view
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_description
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_loading
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_loading_shimmer
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_duration
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_rating
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_type
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_meta_year
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_more_info
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_poster
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_result
|
||||
import kotlinx.android.synthetic.main.bottom_resultview_preview.resultview_preview_title
|
||||
import kotlinx.android.synthetic.main.fragment_result_swipe.media_route_button
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
@ -334,7 +335,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
this@with.runOnUiThread {
|
||||
try {
|
||||
showToast(
|
||||
this@with,
|
||||
getString(if (isSuccessful) R.string.authenticated_user else R.string.authenticated_user_fail).format(
|
||||
api.name
|
||||
)
|
||||
|
@ -362,8 +362,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
// Use both navigation views to support both layouts.
|
||||
// It might be better to use the QuickSearch.
|
||||
nav_view?.selectedItemId = R.id.navigation_search
|
||||
nav_rail_view?.selectedItemId = R.id.navigation_search
|
||||
activity?.findViewById<BottomNavigationView>(R.id.nav_view)?.selectedItemId =
|
||||
R.id.navigation_search
|
||||
activity?.findViewById<NavigationRailView>(R.id.nav_rail_view)?.selectedItemId =
|
||||
R.id.navigation_search
|
||||
} else if (safeURI(str)?.scheme == appStringPlayer) {
|
||||
val uri = Uri.parse(str)
|
||||
val name = uri.getQueryParameter("name")
|
||||
|
@ -396,10 +398,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
this.navigate(R.id.navigation_downloads)
|
||||
return true
|
||||
} else {
|
||||
for (api in apis) {
|
||||
if (str.startsWith(api.mainUrl)) {
|
||||
loadResult(str, api.name)
|
||||
return true
|
||||
synchronized(apis) {
|
||||
for (api in apis) {
|
||||
if (str.startsWith(api.mainUrl)) {
|
||||
loadResult(str, api.name)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -440,7 +444,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
this.hideKeyboard()
|
||||
|
||||
// Fucks up anime info layout since that has its own layout
|
||||
cast_mini_controller_holder?.isVisible =
|
||||
binding?.castMiniControllerHolder?.isVisible =
|
||||
!listOf(
|
||||
R.id.navigation_results_phone,
|
||||
R.id.navigation_results_tv,
|
||||
|
@ -476,15 +480,27 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
R.id.navigation_player,
|
||||
).contains(destination.id)
|
||||
|
||||
nav_host_fragment?.apply {
|
||||
binding?.navHostFragment?.apply {
|
||||
val params = layoutParams as ConstraintLayout.LayoutParams
|
||||
val push =
|
||||
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
|
||||
|
||||
if (!this.isLtr()) {
|
||||
params.setMargins(
|
||||
params.leftMargin,
|
||||
params.topMargin,
|
||||
push,
|
||||
params.bottomMargin
|
||||
)
|
||||
} else {
|
||||
params.setMargins(
|
||||
push,
|
||||
params.topMargin,
|
||||
params.rightMargin,
|
||||
params.bottomMargin
|
||||
)
|
||||
}
|
||||
|
||||
params.setMargins(
|
||||
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0,
|
||||
params.topMargin,
|
||||
params.rightMargin,
|
||||
params.bottomMargin
|
||||
)
|
||||
layoutParams = params
|
||||
}
|
||||
|
||||
|
@ -494,21 +510,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
|
||||
Configuration.ORIENTATION_PORTRAIT -> {
|
||||
false
|
||||
isTvSettings()
|
||||
}
|
||||
|
||||
else -> {
|
||||
false
|
||||
}
|
||||
}
|
||||
binding?.apply {
|
||||
navView.isVisible = isNavVisible && !landscape
|
||||
navRailView.isVisible = isNavVisible && landscape
|
||||
|
||||
nav_view?.isVisible = isNavVisible && !landscape
|
||||
nav_rail_view?.isVisible = isNavVisible && landscape
|
||||
|
||||
// Hide library on TV since it is not supported yet :(
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
nav_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
nav_rail_view?.menu?.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
// Hide library on TV since it is not supported yet :(
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv
|
||||
}
|
||||
}
|
||||
|
||||
//private var mCastSession: CastSession? = null
|
||||
|
@ -576,11 +593,23 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||
CommonActivity.dispatchKeyEvent(this, event)?.let {
|
||||
return it
|
||||
val start = System.currentTimeMillis()
|
||||
try {
|
||||
val response = CommonActivity.dispatchKeyEvent(this, event)
|
||||
|
||||
if (response != null)
|
||||
return response
|
||||
} finally {
|
||||
debugAssert({
|
||||
val end = System.currentTimeMillis()
|
||||
val delta = end - start
|
||||
delta > 100
|
||||
}) {
|
||||
"Took over 100ms to navigate, smth is VERY wrong"
|
||||
}
|
||||
}
|
||||
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
|
@ -685,27 +714,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
private fun onAllPluginsLoaded(success: Boolean = false) {
|
||||
ioSafe {
|
||||
pluginsLock.withLock {
|
||||
// Load cloned sites after plugins have been loaded since clones depend on plugins.
|
||||
try {
|
||||
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
|
||||
list.forEach { custom ->
|
||||
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
||||
?.let {
|
||||
allProviders.add(it.javaClass.newInstance().apply {
|
||||
name = custom.name
|
||||
lang = custom.lang
|
||||
mainUrl = custom.url.trimEnd('/')
|
||||
canBeOverridden = false
|
||||
})
|
||||
}
|
||||
synchronized(allProviders) {
|
||||
// Load cloned sites after plugins have been loaded since clones depend on plugins.
|
||||
try {
|
||||
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
|
||||
list.forEach { custom ->
|
||||
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
||||
?.let {
|
||||
allProviders.add(it.javaClass.newInstance().apply {
|
||||
name = custom.name
|
||||
lang = custom.lang
|
||||
mainUrl = custom.url.trimEnd('/')
|
||||
canBeOverridden = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// it.hashCode() is not enough to make sure they are distinct
|
||||
apis =
|
||||
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
||||
APIHolder.apiMap = null
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
// it.hashCode() is not enough to make sure they are distinct
|
||||
apis =
|
||||
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
||||
APIHolder.apiMap = null
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -721,28 +752,217 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
|
||||
private fun hidePreviewPopupDialog() {
|
||||
viewModel.clear()
|
||||
bottomPreviewPopup.dismissSafe(this)
|
||||
bottomPreviewPopup = null
|
||||
bottomPreviewBinding = null
|
||||
}
|
||||
|
||||
var bottomPreviewPopup: BottomSheetDialog? = null
|
||||
private fun showPreviewPopupDialog(): BottomSheetDialog {
|
||||
val ret = (bottomPreviewPopup ?: run {
|
||||
private var bottomPreviewPopup: BottomSheetDialog? = null
|
||||
private var bottomPreviewBinding: BottomResultviewPreviewBinding? = null
|
||||
private fun showPreviewPopupDialog(): BottomResultviewPreviewBinding {
|
||||
val ret = (bottomPreviewBinding ?: run {
|
||||
val builder =
|
||||
BottomSheetDialog(this)
|
||||
builder.setContentView(R.layout.bottom_resultview_preview)
|
||||
val binding: BottomResultviewPreviewBinding =
|
||||
BottomResultviewPreviewBinding.inflate(builder.layoutInflater, null, false)
|
||||
bottomPreviewBinding = binding
|
||||
builder.setContentView(binding.root)
|
||||
builder.setOnDismissListener {
|
||||
bottomPreviewPopup = null
|
||||
bottomPreviewBinding = null
|
||||
viewModel.clear()
|
||||
}
|
||||
builder.setCanceledOnTouchOutside(true)
|
||||
builder.show()
|
||||
builder
|
||||
bottomPreviewPopup = builder
|
||||
binding
|
||||
})
|
||||
bottomPreviewPopup = ret
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
private var binding: ActivityMainBinding? = null
|
||||
|
||||
object TvFocus {
|
||||
data class FocusTarget(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val x: Float,
|
||||
val y: Float,
|
||||
) {
|
||||
companion object {
|
||||
fun lerp(a: FocusTarget, b: FocusTarget, lerp: Float): FocusTarget {
|
||||
val ilerp = 1 - lerp
|
||||
return FocusTarget(
|
||||
width = (a.width * ilerp + b.width * lerp).toInt(),
|
||||
height = (a.height * ilerp + b.height * lerp).toInt(),
|
||||
x = a.x * ilerp + b.x * lerp,
|
||||
y = a.y * ilerp + b.y * lerp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var last: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
|
||||
var current: FocusTarget = FocusTarget(0, 0, 0.0f, 0.0f)
|
||||
|
||||
var focusOutline: WeakReference<View> = WeakReference(null)
|
||||
var lastFocus: WeakReference<View> = WeakReference(null)
|
||||
private val layoutListener: View.OnLayoutChangeListener =
|
||||
View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
|
||||
updateFocusView(
|
||||
v, same = true
|
||||
)
|
||||
}
|
||||
private val attachListener: View.OnAttachStateChangeListener =
|
||||
object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
updateFocusView(v)
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) {
|
||||
// removes the focus view but not the listener as updateFocusView(null) will remove the listener
|
||||
focusOutline.get()?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun setTargetPosition(target: FocusTarget) {
|
||||
focusOutline.get()?.apply {
|
||||
layoutParams = layoutParams?.apply {
|
||||
width = target.width
|
||||
height = target.height
|
||||
}
|
||||
|
||||
translationX = target.x
|
||||
translationY = target.y
|
||||
bringToFront()
|
||||
}
|
||||
}
|
||||
|
||||
private var animator: ValueAnimator? = null
|
||||
|
||||
@MainThread
|
||||
fun updateFocusView(newFocus: View?, same: Boolean = false) {
|
||||
val focusOutline = focusOutline.get() ?: return
|
||||
lastFocus.get()?.apply {
|
||||
removeOnLayoutChangeListener(layoutListener)
|
||||
removeOnAttachStateChangeListener(attachListener)
|
||||
}
|
||||
|
||||
val wasGone = focusOutline.isGone
|
||||
|
||||
val visible =
|
||||
newFocus != null && newFocus.measuredHeight > 0 && newFocus.measuredWidth > 0 && newFocus.isShown && newFocus.tag != "tv_no_focus_tag"
|
||||
focusOutline.isVisible = visible
|
||||
|
||||
if (newFocus != null) {
|
||||
lastFocus = WeakReference(newFocus)
|
||||
|
||||
val out = IntArray(2)
|
||||
newFocus.getLocationInWindow(out)
|
||||
val (screenX, screenY) = out
|
||||
var (x, y) = screenX.toFloat() to screenY.toFloat()
|
||||
val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY
|
||||
// println(">><<< $x $y $currentX $currentY")
|
||||
if (!newFocus.isLtr()) {
|
||||
x = x - focusOutline.rootView.width + newFocus.measuredWidth
|
||||
}
|
||||
|
||||
// out of bounds = 0,0
|
||||
if (screenX == 0 && screenY == 0) {
|
||||
focusOutline.isVisible = false
|
||||
}
|
||||
|
||||
newFocus.addOnLayoutChangeListener(layoutListener)
|
||||
newFocus.addOnAttachStateChangeListener(attachListener)
|
||||
|
||||
val start = FocusTarget(
|
||||
x = currentX,
|
||||
y = currentY,
|
||||
width = focusOutline.measuredWidth,
|
||||
height = focusOutline.measuredHeight
|
||||
)
|
||||
val end = FocusTarget(
|
||||
x = x,
|
||||
y = y,
|
||||
width = newFocus.measuredWidth,
|
||||
height = newFocus.measuredHeight
|
||||
)
|
||||
|
||||
// if they are the same within then snap, aka scrolling
|
||||
val deltaMin = 50.toPx
|
||||
if (start.width == end.width && start.height == end.height && (start.x - end.x).absoluteValue < deltaMin && (start.y - end.y).absoluteValue < deltaMin) {
|
||||
animator?.cancel()
|
||||
last = start
|
||||
current = end
|
||||
setTargetPosition(end)
|
||||
return
|
||||
}
|
||||
|
||||
// if running then "reuse"
|
||||
if (animator?.isRunning == true) {
|
||||
current = end
|
||||
return
|
||||
} else {
|
||||
animator?.cancel()
|
||||
}
|
||||
|
||||
|
||||
last = start
|
||||
current = end
|
||||
|
||||
// if previously gone, then tp
|
||||
if (wasGone) {
|
||||
setTargetPosition(current)
|
||||
return
|
||||
}
|
||||
|
||||
// animate between a and b
|
||||
animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply {
|
||||
startDelay = 0
|
||||
duration = 100
|
||||
addUpdateListener { animation ->
|
||||
val animatedValue = animation.animatedValue as Float
|
||||
val target = FocusTarget.lerp(last, current, minOf(animatedValue, 1.0f))
|
||||
setTargetPosition(target)
|
||||
}
|
||||
start()
|
||||
}
|
||||
|
||||
// post check
|
||||
if (!same) {
|
||||
newFocus.postDelayed({
|
||||
updateFocusView(lastFocus.get(), same = true)
|
||||
}, 200)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
the following is working, but somewhat bad code code
|
||||
|
||||
if (!wasGone) {
|
||||
(focusOutline.parent as? ViewGroup)?.let {
|
||||
TransitionManager.endTransitions(it)
|
||||
TransitionManager.beginDelayedTransition(
|
||||
it,
|
||||
TransitionSet().addTransition(ChangeBounds())
|
||||
.addTransition(ChangeTransform())
|
||||
.setDuration(100)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
focusOutline.layoutParams = focusOutline.layoutParams?.apply {
|
||||
width = newFocus.measuredWidth
|
||||
height = newFocus.measuredHeight
|
||||
}
|
||||
focusOutline.translationX = x.toFloat()
|
||||
focusOutline.translationY = y.toFloat()*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
app.initClient(this)
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
@ -767,16 +987,48 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
if (isCastApiAvailable()) {
|
||||
mSessionManager = CastContext.getSharedInstance(this).sessionManager
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||
updateTv()
|
||||
if (isTvSettings()) {
|
||||
setContentView(R.layout.activity_main_tv)
|
||||
} else {
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
// backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting?
|
||||
try {
|
||||
val appVer = BuildConfig.VERSION_NAME
|
||||
val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: ""
|
||||
if (appVer != lastAppAutoBackup) {
|
||||
setKey("VERSION_NAME", BuildConfig.VERSION_NAME)
|
||||
backup()
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
|
||||
// just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
|
||||
binding = try {
|
||||
if (isTvSettings()) {
|
||||
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
|
||||
setContentView(newLocalBinding.root)
|
||||
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
|
||||
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
|
||||
// println("refocus $oldFocus -> $newFocus")
|
||||
TvFocus.updateFocusView(newFocus)
|
||||
}
|
||||
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
|
||||
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
|
||||
}
|
||||
|
||||
ActivityMainBinding.bind(newLocalBinding.root) // this may crash
|
||||
} else {
|
||||
val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false)
|
||||
setContentView(newLocalBinding.root)
|
||||
newLocalBinding
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
showToast(txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG)
|
||||
null
|
||||
}
|
||||
|
||||
changeStatusBarState(isEmulatorSettings())
|
||||
|
@ -807,7 +1059,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
if (PluginManager.checkSafeModeFile()) {
|
||||
normalSafeApiCall {
|
||||
showToast(this, R.string.safe_mode_file, Toast.LENGTH_LONG)
|
||||
showToast(R.string.safe_mode_file, Toast.LENGTH_LONG)
|
||||
}
|
||||
} else if (lastError == null) {
|
||||
ioSafe {
|
||||
|
@ -861,43 +1113,44 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
observeNullable(viewModel.page) { resource ->
|
||||
if (resource == null) {
|
||||
bottomPreviewPopup.dismissSafe(this)
|
||||
hidePreviewPopupDialog()
|
||||
return@observeNullable
|
||||
}
|
||||
when (resource) {
|
||||
is Resource.Failure -> {
|
||||
showToast(this, R.string.error)
|
||||
showToast(R.string.error)
|
||||
viewModel.clear()
|
||||
hidePreviewPopupDialog()
|
||||
}
|
||||
|
||||
is Resource.Loading -> {
|
||||
showPreviewPopupDialog().apply {
|
||||
resultview_preview_loading?.isVisible = true
|
||||
resultview_preview_result?.isVisible = false
|
||||
resultview_preview_loading_shimmer?.startShimmer()
|
||||
resultviewPreviewLoading.isVisible = true
|
||||
resultviewPreviewResult.isVisible = false
|
||||
resultviewPreviewLoadingShimmer.startShimmer()
|
||||
}
|
||||
}
|
||||
|
||||
is Resource.Success -> {
|
||||
val d = resource.value
|
||||
showPreviewPopupDialog().apply {
|
||||
resultview_preview_loading?.isVisible = false
|
||||
resultview_preview_result?.isVisible = true
|
||||
resultview_preview_loading_shimmer?.stopShimmer()
|
||||
resultviewPreviewLoading.isVisible = false
|
||||
resultviewPreviewResult.isVisible = true
|
||||
resultviewPreviewLoadingShimmer.stopShimmer()
|
||||
|
||||
resultview_preview_title?.text = d.title
|
||||
resultviewPreviewTitle.text = d.title
|
||||
|
||||
resultview_preview_meta_type.setText(d.typeText)
|
||||
resultview_preview_meta_year.setText(d.yearText)
|
||||
resultview_preview_meta_duration.setText(d.durationText)
|
||||
resultview_preview_meta_rating.setText(d.ratingText)
|
||||
resultviewPreviewMetaType.setText(d.typeText)
|
||||
resultviewPreviewMetaYear.setText(d.yearText)
|
||||
resultviewPreviewMetaDuration.setText(d.durationText)
|
||||
resultviewPreviewMetaRating.setText(d.ratingText)
|
||||
|
||||
resultview_preview_description?.setText(d.plotText)
|
||||
resultview_preview_poster?.setImage(
|
||||
resultviewPreviewDescription.setText(d.plotText)
|
||||
resultviewPreviewPoster.setImage(
|
||||
d.posterImage ?: d.posterBackgroundImage
|
||||
)
|
||||
|
||||
resultview_preview_poster?.setOnClickListener {
|
||||
resultviewPreviewPoster.setOnClickListener {
|
||||
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
|
||||
val value = viewModel.watchStatus.value ?: WatchType.NONE
|
||||
|
||||
|
@ -908,12 +1161,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
showApply = false,
|
||||
{}) {
|
||||
viewModel.updateWatchStatus(WatchType.values()[it])
|
||||
bookmarksUpdatedEvent(true)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTvSettings()) // dont want this clickable on tv layout
|
||||
resultview_preview_description?.setOnClickListener { view ->
|
||||
resultviewPreviewDescription.setOnClickListener { view ->
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
|
@ -923,7 +1175,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}
|
||||
}
|
||||
|
||||
resultview_preview_more_info?.setOnClickListener {
|
||||
resultviewPreviewMoreInfo.setOnClickListener {
|
||||
viewModel.clear()
|
||||
hidePreviewPopupDialog()
|
||||
lastPopup?.let {
|
||||
loadSearchResult(it)
|
||||
|
@ -964,7 +1217,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
ioSafe {
|
||||
initAll()
|
||||
// No duplicates (which can happen by registerMainAPI)
|
||||
apis = allProviders.distinctBy { it }
|
||||
apis = synchronized(allProviders) {
|
||||
allProviders.distinctBy { it }
|
||||
}
|
||||
}
|
||||
|
||||
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
||||
|
@ -977,6 +1232,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
navController.addOnDestinationChangedListener { _: NavController, navDestination: NavDestination, bundle: Bundle? ->
|
||||
// Intercept search and add a query
|
||||
updateNavBar(navDestination)
|
||||
if (navDestination.matchDestination(R.id.navigation_search) && !nextSearchQuery.isNullOrBlank()) {
|
||||
bundle?.apply {
|
||||
this.putString(SearchFragment.SEARCH_QUERY, nextSearchQuery)
|
||||
|
@ -995,29 +1251,47 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
.setPopExitAnim(R.anim.nav_pop_exit)
|
||||
.setPopUpTo(navController.graph.startDestination, false)
|
||||
.build()*/
|
||||
nav_view?.setupWithNavController(navController)
|
||||
val nav_rail = findViewById<NavigationRailView?>(R.id.nav_rail_view)
|
||||
nav_rail?.setupWithNavController(navController)
|
||||
if (isTvSettings()) {
|
||||
nav_rail?.background?.alpha = 200
|
||||
} else {
|
||||
nav_rail?.background?.alpha = 255
|
||||
|
||||
val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
|
||||
|
||||
binding?.navView?.apply {
|
||||
itemRippleColor = rippleColor
|
||||
itemActiveIndicatorColor = rippleColor
|
||||
setupWithNavController(navController)
|
||||
setOnItemSelectedListener { item ->
|
||||
onNavDestinationSelected(
|
||||
item,
|
||||
navController
|
||||
)
|
||||
}
|
||||
}
|
||||
nav_rail?.setOnItemSelectedListener { item ->
|
||||
onNavDestinationSelected(
|
||||
item,
|
||||
navController
|
||||
)
|
||||
}
|
||||
nav_view?.setOnItemSelectedListener { item ->
|
||||
onNavDestinationSelected(
|
||||
item,
|
||||
navController
|
||||
)
|
||||
}
|
||||
navController.addOnDestinationChangedListener { _, destination, _ ->
|
||||
updateNavBar(destination)
|
||||
|
||||
binding?.navRailView?.apply {
|
||||
itemRippleColor = rippleColor
|
||||
itemActiveIndicatorColor = rippleColor
|
||||
setupWithNavController(navController)
|
||||
if (isTvSettings()) {
|
||||
background?.alpha = 200
|
||||
} else {
|
||||
background?.alpha = 255
|
||||
}
|
||||
|
||||
setOnItemSelectedListener { item ->
|
||||
onNavDestinationSelected(
|
||||
item,
|
||||
navController
|
||||
)
|
||||
}
|
||||
|
||||
fun noFocus(view: View) {
|
||||
view.tag = view.context.getString(R.string.tv_no_focus_tag)
|
||||
(view as? ViewGroup)?.let {
|
||||
for (child in it.children) {
|
||||
noFocus(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
noFocus(this)
|
||||
}
|
||||
|
||||
loadCache()
|
||||
|
@ -1040,17 +1314,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
true
|
||||
}*/
|
||||
|
||||
val rippleColor = ColorStateList.valueOf(getResourceColor(R.attr.colorPrimary, 0.1f))
|
||||
nav_view?.itemRippleColor = rippleColor
|
||||
nav_rail?.itemRippleColor = rippleColor
|
||||
nav_rail?.itemActiveIndicatorColor = rippleColor
|
||||
nav_view?.itemActiveIndicatorColor = rippleColor
|
||||
|
||||
if (!checkWrite()) {
|
||||
requestRW()
|
||||
if (checkWrite()) return
|
||||
}
|
||||
CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
|
||||
//CastButtonFactory.setUpMediaRouteButton(this, media_route_button)
|
||||
|
||||
// THIS IS CURRENTLY REMOVED BECAUSE HIGHER VERS OF ANDROID NEEDS A NOTIFICATION
|
||||
//if (!VideoDownloadManager.isMyServiceRunning(this, VideoDownloadKeepAliveService::class.java)) {
|
||||
|
@ -1117,14 +1386,15 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
if (BuildConfig.DEBUG) {
|
||||
var providersAndroidManifestString = "Current androidmanifest should be:\n"
|
||||
for (api in allProviders) {
|
||||
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
|
||||
api.mainUrl.removePrefix(
|
||||
"https://"
|
||||
)
|
||||
}\" android:pathPrefix=\"/\"/>\n"
|
||||
synchronized(allProviders) {
|
||||
for (api in allProviders) {
|
||||
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
|
||||
api.mainUrl.removePrefix(
|
||||
"https://"
|
||||
)
|
||||
}\" android:pathPrefix=\"/\"/>\n"
|
||||
}
|
||||
}
|
||||
|
||||
println(providersAndroidManifestString)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package com.lagradost.cloudstream3.extractors
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.getAndUnpack
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||
|
||||
open class StreamoUpload : ExtractorApi() {
|
||||
override val name = "StreamoUpload"
|
||||
override val mainUrl = "https://streamoupload.xyz"
|
||||
override val requiresReferer = true
|
||||
|
||||
override suspend fun getUrl(url: String, referer: String?): List<ExtractorLink> {
|
||||
val sources = mutableListOf<ExtractorLink>()
|
||||
val response = app.get(url, referer = referer)
|
||||
val scriptElements = response.document.select("script").map { script ->
|
||||
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||
val data = getAndUnpack(script.data())
|
||||
.substringAfter("sources:[")
|
||||
.substringBefore("],")
|
||||
.replace("file", "\"file\"")
|
||||
.trim()
|
||||
tryParseJson<File>(data)?.let {
|
||||
M3u8Helper.generateM3u8(
|
||||
name,
|
||||
it.file,
|
||||
"$mainUrl/",
|
||||
).forEach { m3uData -> sources.add(m3uData) }
|
||||
}
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
private data class File(
|
||||
@JsonProperty("file") val file: String,
|
||||
)
|
||||
}
|
|
@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||
}
|
||||
|
||||
private val validApis by lazy {
|
||||
apis.filter { it.lang == this.lang && it::class.java != this::class.java }
|
||||
//.distinctBy { it.uniqueId }
|
||||
}
|
||||
private val validApis
|
||||
get() =
|
||||
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
|
||||
//.distinctBy { it.uniqueId }
|
||||
|
||||
|
||||
data class CrossMetaData(
|
||||
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
||||
|
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val base = super.load(url)?.apply {
|
||||
this.recommendations = this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||
this.recommendations =
|
||||
this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||
val matchName = filterName(this.name)
|
||||
when (this) {
|
||||
is MovieLoadResponse -> {
|
||||
|
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
this.dataUrl =
|
||||
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
||||
}
|
||||
|
|
|
@ -25,13 +25,16 @@ class MultiAnimeProvider : MainAPI() {
|
|||
}
|
||||
}
|
||||
|
||||
private val validApis by lazy {
|
||||
APIHolder.apis.filter {
|
||||
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
|
||||
TvType.Anime
|
||||
)
|
||||
}
|
||||
}
|
||||
private val validApis
|
||||
get() =
|
||||
synchronized(APIHolder.apis) {
|
||||
APIHolder.apis.filter {
|
||||
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
|
||||
TvType.Anime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun filterName(name: String): String {
|
||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||
|
|
|
@ -57,32 +57,6 @@ fun <T> LifecycleOwner.observeNullable(liveData: LiveData<T>, action: (t: T) ->
|
|||
liveData.observe(this) { action(it) }
|
||||
}
|
||||
|
||||
inline fun <reified T : Any> some(value: T?): Some<T> {
|
||||
return if (value == null) {
|
||||
Some.None
|
||||
} else {
|
||||
Some.Success(value)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Some<out T> {
|
||||
data class Success<out T>(val value: T) : Some<T>()
|
||||
object None : Some<Nothing>()
|
||||
|
||||
override fun toString(): String {
|
||||
return when (this) {
|
||||
is None -> "None"
|
||||
is Success -> "Some(${value.toString()})"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ResourceSome<out T> {
|
||||
data class Success<out T>(val value: T) : ResourceSome<T>()
|
||||
object None : ResourceSome<Nothing>()
|
||||
data class Loading(val data: Any? = null) : ResourceSome<Nothing>()
|
||||
}
|
||||
|
||||
sealed class Resource<out T> {
|
||||
data class Success<out T>(val value: T) : Resource<T>()
|
||||
data class Failure(
|
||||
|
@ -155,6 +129,70 @@ fun CoroutineScope.launchSafe(
|
|||
return this.launch(context, start, obj)
|
||||
}
|
||||
|
||||
fun<T> throwAbleToResource(
|
||||
throwable: Throwable
|
||||
): Resource<T> {
|
||||
return when (throwable) {
|
||||
is NullPointerException -> {
|
||||
for (line in throwable.stackTrace) {
|
||||
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
|
||||
return Resource.Failure(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
|
||||
)
|
||||
}
|
||||
}
|
||||
safeFail(throwable)
|
||||
}
|
||||
is SocketTimeoutException, is InterruptedIOException -> {
|
||||
Resource.Failure(
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
"Connection Timeout\nPlease try again later."
|
||||
)
|
||||
}
|
||||
is HttpException -> {
|
||||
Resource.Failure(
|
||||
false,
|
||||
throwable.statusCode,
|
||||
null,
|
||||
throwable.message ?: "HttpException"
|
||||
)
|
||||
}
|
||||
is UnknownHostException -> {
|
||||
Resource.Failure(true, null, null, "Cannot connect to server, try again later.\n${throwable.message}")
|
||||
}
|
||||
is ErrorLoadingException -> {
|
||||
Resource.Failure(
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
throwable.message ?: "Error loading, try again later."
|
||||
)
|
||||
}
|
||||
is NotImplementedError -> {
|
||||
Resource.Failure(false, null, null, "This operation is not implemented.")
|
||||
}
|
||||
is SSLHandshakeException -> {
|
||||
Resource.Failure(
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
|
||||
)
|
||||
}
|
||||
is CancellationException -> {
|
||||
throwable.cause?.let {
|
||||
throwAbleToResource(it)
|
||||
} ?: safeFail(throwable)
|
||||
}
|
||||
else -> safeFail(throwable)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> safeApiCall(
|
||||
apiCall: suspend () -> T,
|
||||
): Resource<T> {
|
||||
|
@ -163,60 +201,7 @@ suspend fun <T> safeApiCall(
|
|||
Resource.Success(apiCall.invoke())
|
||||
} catch (throwable: Throwable) {
|
||||
logError(throwable)
|
||||
when (throwable) {
|
||||
is NullPointerException -> {
|
||||
for (line in throwable.stackTrace) {
|
||||
if (line?.fileName?.endsWith("provider.kt", ignoreCase = true) == true) {
|
||||
return@withContext Resource.Failure(
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
"NullPointerException at ${line.fileName} ${line.lineNumber}\nSite might have updated or added Cloudflare/DDOS protection"
|
||||
)
|
||||
}
|
||||
}
|
||||
safeFail(throwable)
|
||||
}
|
||||
is SocketTimeoutException, is InterruptedIOException -> {
|
||||
Resource.Failure(
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
"Connection Timeout\nPlease try again later."
|
||||
)
|
||||
}
|
||||
is HttpException -> {
|
||||
Resource.Failure(
|
||||
false,
|
||||
throwable.statusCode,
|
||||
null,
|
||||
throwable.message ?: "HttpException"
|
||||
)
|
||||
}
|
||||
is UnknownHostException -> {
|
||||
Resource.Failure(true, null, null, "Cannot connect to server, try again later.")
|
||||
}
|
||||
is ErrorLoadingException -> {
|
||||
Resource.Failure(
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
throwable.message ?: "Error loading, try again later."
|
||||
)
|
||||
}
|
||||
is NotImplementedError -> {
|
||||
Resource.Failure(false, null, null, "This operation is not implemented.")
|
||||
}
|
||||
is SSLHandshakeException -> {
|
||||
Resource.Failure(
|
||||
true,
|
||||
null,
|
||||
null,
|
||||
(throwable.message ?: "SSLHandshakeException") + "\nTry a VPN or DNS."
|
||||
)
|
||||
}
|
||||
else -> safeFail(throwable)
|
||||
}
|
||||
throwAbleToResource(throwable)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,7 +36,9 @@ abstract class Plugin {
|
|||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
||||
element.sourcePlugin = this.__filename
|
||||
// Race condition causing which would case duplicates if not for distinctBy
|
||||
APIHolder.allProviders.add(element)
|
||||
synchronized(APIHolder.allProviders) {
|
||||
APIHolder.allProviders.add(element)
|
||||
}
|
||||
APIHolder.addPluginMapping(element)
|
||||
}
|
||||
|
||||
|
@ -51,10 +53,14 @@ abstract class Plugin {
|
|||
}
|
||||
|
||||
class Manifest {
|
||||
@JsonProperty("name") var name: String? = null
|
||||
@JsonProperty("pluginClassName") var pluginClassName: String? = null
|
||||
@JsonProperty("version") var version: Int? = null
|
||||
@JsonProperty("requiresResources") var requiresResources: Boolean = false
|
||||
@JsonProperty("name")
|
||||
var name: String? = null
|
||||
@JsonProperty("pluginClassName")
|
||||
var pluginClassName: String? = null
|
||||
@JsonProperty("version")
|
||||
var version: Int? = null
|
||||
@JsonProperty("requiresResources")
|
||||
var requiresResources: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -163,7 +163,8 @@ object PluginManager {
|
|||
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
|
||||
HashMap<PathClassLoader, Plugin>()
|
||||
|
||||
private var loadedLocalPlugins = false
|
||||
var loadedLocalPlugins = false
|
||||
private set
|
||||
private val gson = Gson()
|
||||
|
||||
private suspend fun maybeLoadPlugin(context: Context, file: File) {
|
||||
|
@ -531,10 +532,14 @@ object PluginManager {
|
|||
}
|
||||
|
||||
// remove all registered apis
|
||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
||||
removePluginMapping(it)
|
||||
synchronized(APIHolder.apis) {
|
||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
||||
removePluginMapping(it)
|
||||
}
|
||||
}
|
||||
synchronized(APIHolder.allProviders) {
|
||||
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
||||
}
|
||||
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
||||
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
|
||||
|
||||
classLoaders.values.removeIf { v -> v == plugin }
|
||||
|
|
|
@ -24,7 +24,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onRequestChildFocus(
|
||||
/*override fun onRequestChildFocus(
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State,
|
||||
child: View,
|
||||
|
@ -32,13 +32,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
|||
): Boolean {
|
||||
// android.widget.FrameLayout$LayoutParams cannot be cast to androidx.recyclerview.widget.RecyclerView$LayoutParams
|
||||
return try {
|
||||
val pos = maxOf(0, getPosition(focused!!) - 2)
|
||||
parent.scrollToPosition(pos)
|
||||
if(focused != null) {
|
||||
// val pos = maxOf(0, getPosition(focused) - 2) // IDK WHY
|
||||
val pos = getPosition(focused)
|
||||
if(pos >= 0) parent.scrollToPosition(pos)
|
||||
}
|
||||
|
||||
super.onRequestChildFocus(parent, state, child, focused)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// Allows moving right and left with focus https://gist.github.com/vganin/8930b41f55820ec49e4d
|
||||
override fun onInterceptFocusSearch(focused: View, direction: Int): View? {
|
||||
|
@ -65,8 +69,17 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
|||
val spanCount = this.spanCount
|
||||
val orientation = this.orientation
|
||||
|
||||
// fixes arabic by inverting left and right layout focus
|
||||
val correctDirection = if(this.isLayoutRTL) {
|
||||
when(direction) {
|
||||
View.FOCUS_RIGHT -> View.FOCUS_LEFT
|
||||
View.FOCUS_LEFT -> View.FOCUS_RIGHT
|
||||
else -> direction
|
||||
}
|
||||
} else direction
|
||||
|
||||
if (orientation == VERTICAL) {
|
||||
when (direction) {
|
||||
when (correctDirection) {
|
||||
View.FOCUS_DOWN -> {
|
||||
return spanCount
|
||||
}
|
||||
|
@ -81,7 +94,7 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) :
|
|||
}
|
||||
}
|
||||
} else if (orientation == HORIZONTAL) {
|
||||
when (direction) {
|
||||
when (correctDirection) {
|
||||
View.FOCUS_DOWN -> {
|
||||
return 1
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -12,20 +12,23 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.USER_AGENT
|
||||
import com.lagradost.cloudstream3.databinding.FragmentWebviewBinding
|
||||
import com.lagradost.cloudstream3.network.WebViewResolver
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadRepository
|
||||
import kotlinx.android.synthetic.main.fragment_webview.*
|
||||
|
||||
|
||||
class WebviewFragment : Fragment() {
|
||||
|
||||
var binding: FragmentWebviewBinding? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val url = arguments?.getString(WEBVIEW_URL) ?: "".also {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
web_view.webViewClient = object : WebViewClient() {
|
||||
binding?.webView?.webViewClient = object : WebViewClient() {
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
|
@ -40,24 +43,28 @@ class WebviewFragment : Fragment() {
|
|||
return super.shouldOverrideUrlLoading(view, request)
|
||||
}
|
||||
}
|
||||
binding?.webView?.apply {
|
||||
WebViewResolver.webViewUserAgent = settings.userAgentString
|
||||
|
||||
WebViewResolver.webViewUserAgent = web_view.settings.userAgentString
|
||||
|
||||
web_view.addJavascriptInterface(RepoApi(activity), "RepoApi")
|
||||
web_view.settings.javaScriptEnabled = true
|
||||
web_view.settings.userAgentString = USER_AGENT
|
||||
web_view.settings.domStorageEnabled = true
|
||||
addJavascriptInterface(RepoApi(activity), "RepoApi")
|
||||
settings.javaScriptEnabled = true
|
||||
settings.userAgentString = USER_AGENT
|
||||
settings.domStorageEnabled = true
|
||||
// WebView.setWebContentsDebuggingEnabled(true)
|
||||
|
||||
web_view.loadUrl(url)
|
||||
loadUrl(url)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
): View {
|
||||
val localBinding = FragmentWebviewBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_webview, container, false)
|
||||
return localBinding.root//inflater.inflate(R.layout.fragment_webview, container, false)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -70,7 +77,7 @@ class WebviewFragment : Fragment() {
|
|||
|
||||
private class RepoApi(val activity: FragmentActivity?) {
|
||||
@JavascriptInterface
|
||||
fun installRepo(repoUrl: String) {
|
||||
fun installRepo(repoUrl: String) {
|
||||
activity?.loadRepository(repoUrl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package com.lagradost.cloudstream3.ui.download
|
||||
|
||||
interface DownloadButtonViewHolder {
|
||||
var downloadButton : EasyDownloadButton
|
||||
fun reattachDownloadButton()
|
||||
}
|
|
@ -3,18 +3,12 @@ package com.lagradost.cloudstream3.ui.download
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.DownloadChildEpisodeBinding
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import kotlinx.android.synthetic.main.download_child_episode.view.*
|
||||
import java.util.*
|
||||
|
||||
const val DOWNLOAD_ACTION_PLAY_FILE = 0
|
||||
const val DOWNLOAD_ACTION_DELETE_FILE = 1
|
||||
|
@ -36,39 +30,9 @@ class DownloadChildAdapter(
|
|||
private val clickCallback: (DownloadClickEvent) -> Unit,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
|
||||
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
|
||||
return Collections.unmodifiableSet(mBoundViewHolders)
|
||||
}
|
||||
|
||||
fun killAdapter() {
|
||||
getAllBoundViewHolders()?.forEach { view ->
|
||||
view?.downloadButton?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.downloadButton.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.downloadButton.dispose()
|
||||
mBoundViewHolders.remove(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.reattachDownloadButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return DownloadChildViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.download_child_episode, parent, false),
|
||||
DownloadChildEpisodeBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
clickCallback
|
||||
)
|
||||
}
|
||||
|
@ -77,7 +41,6 @@ class DownloadChildAdapter(
|
|||
when (holder) {
|
||||
is DownloadChildViewHolder -> {
|
||||
holder.bind(cardList[position])
|
||||
mBoundViewHolders.add(holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,66 +51,44 @@ class DownloadChildAdapter(
|
|||
|
||||
class DownloadChildViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
val binding: DownloadChildEpisodeBinding,
|
||||
private val clickCallback: (DownloadClickEvent) -> Unit,
|
||||
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
|
||||
override var downloadButton = EasyDownloadButton()
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private val title: TextView = itemView.download_child_episode_text
|
||||
/*private val title: TextView = itemView.download_child_episode_text
|
||||
private val extraInfo: TextView = itemView.download_child_episode_text_extra
|
||||
private val holder: CardView = itemView.download_child_episode_holder
|
||||
private val progressBar: ContentLoadingProgressBar = itemView.download_child_episode_progress
|
||||
private val progressBarDownload: ContentLoadingProgressBar = itemView.download_child_episode_progress_downloaded
|
||||
private val downloadImage: ImageView = itemView.download_child_episode_download
|
||||
private val downloadImage: ImageView = itemView.download_child_episode_download*/
|
||||
|
||||
private var localCard: VisualDownloadChildCached? = null
|
||||
|
||||
fun bind(card: VisualDownloadChildCached) {
|
||||
localCard = card
|
||||
val d = card.data
|
||||
|
||||
val posDur = getViewPos(d.id)
|
||||
if (posDur != null) {
|
||||
val visualPos = posDur.fixVisual()
|
||||
progressBar.max = (visualPos.duration / 1000).toInt()
|
||||
progressBar.progress = (visualPos.position / 1000).toInt()
|
||||
progressBar.visibility = View.VISIBLE
|
||||
} else {
|
||||
progressBar.visibility = View.GONE
|
||||
binding.downloadChildEpisodeProgress.apply {
|
||||
if (posDur != null) {
|
||||
val visualPos = posDur.fixVisual()
|
||||
max = (visualPos.duration / 1000).toInt()
|
||||
progress = (visualPos.position / 1000).toInt()
|
||||
visibility = View.VISIBLE
|
||||
} else {
|
||||
visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
title.text = title.context.getNameFull(d.name, d.episode, d.season)
|
||||
title.isSelected = true // is needed for text repeating
|
||||
binding.downloadButton.setDefaultClickListener(card.data, binding.downloadChildEpisodeTextExtra, clickCallback)
|
||||
|
||||
downloadButton.setUpButton(
|
||||
card.currentBytes,
|
||||
card.totalBytes,
|
||||
progressBarDownload,
|
||||
downloadImage,
|
||||
extraInfo,
|
||||
card.data,
|
||||
clickCallback
|
||||
)
|
||||
binding.downloadChildEpisodeText.apply {
|
||||
text = context.getNameFull(d.name, d.episode, d.season)
|
||||
isSelected = true // is needed for text repeating
|
||||
}
|
||||
|
||||
holder.setOnClickListener {
|
||||
|
||||
binding.downloadChildEpisodeHolder.setOnClickListener {
|
||||
clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, d))
|
||||
}
|
||||
}
|
||||
|
||||
override fun reattachDownloadButton() {
|
||||
downloadButton.dispose()
|
||||
val card = localCard
|
||||
if (card != null) {
|
||||
downloadButton.setUpButton(
|
||||
card.currentBytes,
|
||||
card.totalBytes,
|
||||
progressBarDownload,
|
||||
downloadImage,
|
||||
extraInfo,
|
||||
card.data,
|
||||
clickCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentChildDownloadsBinding
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
|
@ -15,13 +16,12 @@ import com.lagradost.cloudstream3.utils.DataStore.getKeys
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import kotlinx.android.synthetic.main.fragment_child_downloads.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class DownloadChildFragment : Fragment() {
|
||||
companion object {
|
||||
fun newInstance(headerName: String, folder: String) : Bundle {
|
||||
fun newInstance(headerName: String, folder: String): Bundle {
|
||||
return Bundle().apply {
|
||||
putString("folder", folder)
|
||||
putString("name", headerName)
|
||||
|
@ -30,13 +30,20 @@ class DownloadChildFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
(download_child_list?.adapter as DownloadChildAdapter?)?.killAdapter()
|
||||
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent -= it }
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_child_downloads, container, false)
|
||||
var binding: FragmentChildDownloadsBinding? = null
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val localBinding = FragmentChildDownloadsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root//inflater.inflate(R.layout.fragment_child_downloads, container, false)
|
||||
}
|
||||
|
||||
private fun updateList(folder: String) = main {
|
||||
|
@ -50,14 +57,15 @@ class DownloadChildFragment : Fragment() {
|
|||
?: return@mapNotNull null
|
||||
VisualDownloadChildCached(info.fileLength, info.totalBytes, it)
|
||||
}
|
||||
}.sortedBy { it.data.episode + (it.data.season?: 0)*100000 }
|
||||
}.sortedBy { it.data.episode + (it.data.season ?: 0) * 100000 }
|
||||
if (eps.isEmpty()) {
|
||||
activity?.onBackPressed()
|
||||
return@main
|
||||
}
|
||||
|
||||
(download_child_list?.adapter as DownloadChildAdapter? ?: return@main).cardList = eps
|
||||
download_child_list?.adapter?.notifyDataSetChanged()
|
||||
(binding?.downloadChildList?.adapter as DownloadChildAdapter? ?: return@main).cardList =
|
||||
eps
|
||||
binding?.downloadChildList?.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,23 +80,26 @@ class DownloadChildFragment : Fragment() {
|
|||
activity?.onBackPressed() // TODO FIX
|
||||
return
|
||||
}
|
||||
context?.fixPaddingStatusbar(download_child_root)
|
||||
fixPaddingStatusbar(binding?.downloadChildRoot)
|
||||
|
||||
download_child_toolbar.title = name
|
||||
download_child_toolbar.setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
download_child_toolbar.setNavigationOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
binding?.downloadChildToolbar?.apply {
|
||||
title = name
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||
DownloadChildAdapter(
|
||||
ArrayList(),
|
||||
) { click ->
|
||||
handleDownloadClick(activity, click)
|
||||
handleDownloadClick(click)
|
||||
}
|
||||
|
||||
downloadDeleteEventListener = { id: Int ->
|
||||
val list = (download_child_list?.adapter as DownloadChildAdapter?)?.cardList
|
||||
val list = (binding?.downloadChildList?.adapter as DownloadChildAdapter?)?.cardList
|
||||
if (list != null) {
|
||||
if (list.any { it.data.id == id }) {
|
||||
updateList(folder)
|
||||
|
@ -98,8 +109,8 @@ class DownloadChildFragment : Fragment() {
|
|||
|
||||
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
|
||||
|
||||
download_child_list.adapter = adapter
|
||||
download_child_list.layoutManager = GridLayoutManager(context, 1)
|
||||
binding?.downloadChildList?.adapter = adapter
|
||||
binding?.downloadChildList?.layoutManager = GridLayoutManager(context, 1)
|
||||
|
||||
updateList(folder)
|
||||
}
|
||||
|
|
|
@ -34,10 +34,10 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import kotlinx.android.synthetic.main.fragment_downloads.*
|
||||
import kotlinx.android.synthetic.main.stream_input.*
|
||||
import android.text.format.Formatter.formatShortFileSize
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
|
||||
import com.lagradost.cloudstream3.databinding.StreamInputBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.ui.player.BasicLink
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
|
@ -60,8 +60,8 @@ class DownloadFragment : Fragment() {
|
|||
|
||||
private fun setList(list: List<VisualDownloadHeaderCached>) {
|
||||
main {
|
||||
(download_list?.adapter as DownloadHeaderAdapter?)?.cardList = list
|
||||
download_list?.adapter?.notifyDataSetChanged()
|
||||
(binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList = list
|
||||
binding?.downloadList?.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,10 +70,12 @@ class DownloadFragment : Fragment() {
|
|||
VideoDownloadManager.downloadDeleteEvent -= downloadDeleteEventListener!!
|
||||
downloadDeleteEventListener = null
|
||||
}
|
||||
(download_list?.adapter as DownloadHeaderAdapter?)?.killAdapter()
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
var binding : FragmentDownloadsBinding? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -82,7 +84,9 @@ class DownloadFragment : Fragment() {
|
|||
downloadsViewModel =
|
||||
ViewModelProvider(this)[DownloadViewModel::class.java]
|
||||
|
||||
return inflater.inflate(R.layout.fragment_downloads, container, false)
|
||||
val localBinding = FragmentDownloadsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root//inflater.inflate(R.layout.fragment_downloads, container, false)
|
||||
}
|
||||
|
||||
private var downloadDeleteEventListener: ((Int) -> Unit)? = null
|
||||
|
@ -92,36 +96,40 @@ class DownloadFragment : Fragment() {
|
|||
hideKeyboard()
|
||||
|
||||
observe(downloadsViewModel.noDownloadsText) {
|
||||
text_no_downloads.text = it
|
||||
binding?.textNoDownloads?.text = it
|
||||
}
|
||||
observe(downloadsViewModel.headerCards) {
|
||||
setList(it)
|
||||
download_loading.isVisible = false
|
||||
binding?.downloadLoading?.isVisible = false
|
||||
}
|
||||
observe(downloadsViewModel.availableBytes) {
|
||||
download_free_txt?.text =
|
||||
binding?.downloadFreeTxt?.text =
|
||||
getString(R.string.storage_size_format).format(
|
||||
getString(R.string.free_storage),
|
||||
formatShortFileSize(view.context, it)
|
||||
)
|
||||
download_free?.setLayoutWidth(it)
|
||||
binding?.downloadFree?.setLayoutWidth(it)
|
||||
}
|
||||
observe(downloadsViewModel.usedBytes) {
|
||||
download_used_txt?.text =
|
||||
getString(R.string.storage_size_format).format(
|
||||
getString(R.string.used_storage),
|
||||
formatShortFileSize(view.context, it)
|
||||
)
|
||||
download_used?.setLayoutWidth(it)
|
||||
download_storage_appbar?.isVisible = it > 0
|
||||
binding?.apply {
|
||||
downloadUsedTxt.text =
|
||||
getString(R.string.storage_size_format).format(
|
||||
getString(R.string.used_storage),
|
||||
formatShortFileSize(view.context, it)
|
||||
)
|
||||
downloadUsed.setLayoutWidth(it)
|
||||
downloadStorageAppbar.isVisible = it > 0
|
||||
}
|
||||
}
|
||||
observe(downloadsViewModel.downloadBytes) {
|
||||
download_app_txt?.text =
|
||||
getString(R.string.storage_size_format).format(
|
||||
getString(R.string.app_storage),
|
||||
formatShortFileSize(view.context, it)
|
||||
)
|
||||
download_app?.setLayoutWidth(it)
|
||||
binding?.apply {
|
||||
downloadAppTxt.text =
|
||||
getString(R.string.storage_size_format).format(
|
||||
getString(R.string.app_storage),
|
||||
formatShortFileSize(view.context, it)
|
||||
)
|
||||
downloadApp.setLayoutWidth(it)
|
||||
}
|
||||
}
|
||||
|
||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||
|
@ -154,7 +162,7 @@ class DownloadFragment : Fragment() {
|
|||
},
|
||||
{ downloadClickEvent ->
|
||||
if (downloadClickEvent.data !is VideoDownloadHelper.DownloadEpisodeCached) return@DownloadHeaderAdapter
|
||||
handleDownloadClick(activity, downloadClickEvent)
|
||||
handleDownloadClick(downloadClickEvent)
|
||||
if (downloadClickEvent.action == DOWNLOAD_ACTION_DELETE_FILE) {
|
||||
context?.let { ctx ->
|
||||
downloadsViewModel.updateList(ctx)
|
||||
|
@ -164,7 +172,7 @@ class DownloadFragment : Fragment() {
|
|||
)
|
||||
|
||||
downloadDeleteEventListener = { id ->
|
||||
val list = (download_list?.adapter as DownloadHeaderAdapter?)?.cardList
|
||||
val list = (binding?.downloadList?.adapter as DownloadHeaderAdapter?)?.cardList
|
||||
if (list != null) {
|
||||
if (list.any { it.data.id == id }) {
|
||||
context?.let { ctx ->
|
||||
|
@ -177,31 +185,36 @@ class DownloadFragment : Fragment() {
|
|||
|
||||
downloadDeleteEventListener?.let { VideoDownloadManager.downloadDeleteEvent += it }
|
||||
|
||||
download_list?.adapter = adapter
|
||||
download_list?.layoutManager = GridLayoutManager(context, 1)
|
||||
binding?.downloadList?.apply {
|
||||
this.adapter = adapter
|
||||
layoutManager = GridLayoutManager(context, 1)
|
||||
}
|
||||
|
||||
// Should be visible in emulator layout
|
||||
download_stream_button?.isGone = isTrueTvSettings()
|
||||
download_stream_button?.setOnClickListener {
|
||||
binding?.downloadStreamButton?.isGone = isTrueTvSettings()
|
||||
binding?.downloadStreamButton?.setOnClickListener {
|
||||
val dialog =
|
||||
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)
|
||||
dialog.setContentView(R.layout.stream_input)
|
||||
|
||||
val binding = StreamInputBinding.inflate(dialog.layoutInflater)
|
||||
|
||||
dialog.setContentView(binding.root)
|
||||
|
||||
dialog.show()
|
||||
|
||||
// If user has clicked the switch do not interfere
|
||||
var preventAutoSwitching = false
|
||||
dialog.hls_switch?.setOnClickListener {
|
||||
binding.hlsSwitch.setOnClickListener {
|
||||
preventAutoSwitching = true
|
||||
}
|
||||
|
||||
fun activateSwitchOnHls(text: String?) {
|
||||
dialog.hls_switch?.isChecked = normalSafeApiCall {
|
||||
binding.hlsSwitch.isChecked = normalSafeApiCall {
|
||||
URI(text).path?.substringAfterLast(".")?.contains("m3u")
|
||||
} == true
|
||||
}
|
||||
|
||||
dialog.stream_referer?.doOnTextChanged { text, _, _, _ ->
|
||||
binding.streamReferer.doOnTextChanged { text, _, _, _ ->
|
||||
if (!preventAutoSwitching)
|
||||
activateSwitchOnHls(text?.toString())
|
||||
}
|
||||
|
@ -210,16 +223,16 @@ class DownloadFragment : Fragment() {
|
|||
0
|
||||
)?.text?.toString()?.let { copy ->
|
||||
val fixedText = copy.trim()
|
||||
dialog.stream_url?.setText(fixedText)
|
||||
binding.streamUrl.setText(fixedText)
|
||||
activateSwitchOnHls(fixedText)
|
||||
}
|
||||
|
||||
dialog.apply_btt?.setOnClickListener {
|
||||
val url = dialog.stream_url.text?.toString()
|
||||
binding.applyBtt.setOnClickListener {
|
||||
val url = binding.streamUrl.text?.toString()
|
||||
if (url.isNullOrEmpty()) {
|
||||
showToast(activity, R.string.error_invalid_url, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.error_invalid_url, Toast.LENGTH_SHORT)
|
||||
} else {
|
||||
val referer = dialog.stream_referer.text?.toString()
|
||||
val referer = binding.streamReferer.text?.toString()
|
||||
|
||||
activity?.navigate(
|
||||
R.id.global_to_navigation_player,
|
||||
|
@ -228,7 +241,7 @@ class DownloadFragment : Fragment() {
|
|||
listOf(BasicLink(url)),
|
||||
extract = true,
|
||||
referer = referer,
|
||||
isM3u8 = dialog.hls_switch?.isChecked
|
||||
isM3u8 = binding.hlsSwitch.isChecked
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -237,22 +250,22 @@ class DownloadFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
dialog.cancel_btt?.setOnClickListener {
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
download_list?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
binding?.downloadList?.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
val dy = scrollY - oldScrollY
|
||||
if (dy > 0) { //check for scroll down
|
||||
download_stream_button?.shrink() // hide
|
||||
binding?.downloadStreamButton?.shrink() // hide
|
||||
} else if (dy < -5) {
|
||||
download_stream_button?.extend() // show
|
||||
binding?.downloadStreamButton?.extend() // show
|
||||
}
|
||||
}
|
||||
}
|
||||
downloadsViewModel.updateList(requireContext())
|
||||
|
||||
context?.fixPaddingStatusbar(download_root)
|
||||
fixPaddingStatusbar(binding?.downloadRoot)
|
||||
}
|
||||
}
|
|
@ -5,16 +5,13 @@ import android.text.format.Formatter.formatShortFileSize
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.DownloadHeaderEpisodeBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import kotlinx.android.synthetic.main.download_header_episode.view.*
|
||||
import java.util.*
|
||||
|
||||
data class VisualDownloadHeaderCached(
|
||||
|
@ -26,7 +23,10 @@ data class VisualDownloadHeaderCached(
|
|||
val child: VideoDownloadHelper.DownloadEpisodeCached?,
|
||||
)
|
||||
|
||||
data class DownloadHeaderClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadHeaderCached)
|
||||
data class DownloadHeaderClickEvent(
|
||||
val action: Int,
|
||||
val data: VideoDownloadHelper.DownloadHeaderCached
|
||||
)
|
||||
|
||||
class DownloadHeaderAdapter(
|
||||
var cardList: List<VisualDownloadHeaderCached>,
|
||||
|
@ -34,39 +34,13 @@ class DownloadHeaderAdapter(
|
|||
private val movieClickCallback: (DownloadClickEvent) -> Unit,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
|
||||
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
|
||||
return Collections.unmodifiableSet(mBoundViewHolders)
|
||||
}
|
||||
|
||||
fun killAdapter() {
|
||||
getAllBoundViewHolders()?.forEach { view ->
|
||||
view?.downloadButton?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.downloadButton.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.downloadButton.dispose()
|
||||
mBoundViewHolders.remove(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.reattachDownloadButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return DownloadHeaderViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.download_header_episode, parent, false),
|
||||
DownloadHeaderEpisodeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
clickCallback,
|
||||
movieClickCallback
|
||||
)
|
||||
|
@ -76,7 +50,6 @@ class DownloadHeaderAdapter(
|
|||
when (holder) {
|
||||
is DownloadHeaderViewHolder -> {
|
||||
holder.bind(cardList[position])
|
||||
mBoundViewHolders.add(holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,93 +60,89 @@ class DownloadHeaderAdapter(
|
|||
|
||||
class DownloadHeaderViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
val binding: DownloadHeaderEpisodeBinding,
|
||||
private val clickCallback: (DownloadHeaderClickEvent) -> Unit,
|
||||
private val movieClickCallback: (DownloadClickEvent) -> Unit,
|
||||
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
|
||||
override var downloadButton = EasyDownloadButton()
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private val poster: ImageView? = itemView.download_header_poster
|
||||
/*private val poster: ImageView? = itemView.download_header_poster
|
||||
private val title: TextView = itemView.download_header_title
|
||||
private val extraInfo: TextView = itemView.download_header_info
|
||||
private val holder: CardView = itemView.episode_holder
|
||||
|
||||
private val downloadBar: ContentLoadingProgressBar = itemView.download_header_progress_downloaded
|
||||
private val downloadImage: ImageView = itemView.download_header_episode_download
|
||||
private val normalImage: ImageView = itemView.download_header_goto_child
|
||||
private var localCard: VisualDownloadHeaderCached? = null
|
||||
private val normalImage: ImageView = itemView.download_header_goto_child*/
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(card: VisualDownloadHeaderCached) {
|
||||
localCard = card
|
||||
val d = card.data
|
||||
|
||||
poster?.setImage(d.poster)
|
||||
poster?.setOnClickListener {
|
||||
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
|
||||
binding.downloadHeaderPoster.apply {
|
||||
setImage(d.poster)
|
||||
setOnClickListener {
|
||||
clickCallback.invoke(DownloadHeaderClickEvent(1, d))
|
||||
}
|
||||
}
|
||||
|
||||
title.text = d.name
|
||||
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
|
||||
binding.apply {
|
||||
|
||||
//val isMovie = d.type.isMovieType()
|
||||
if (card.child != null) {
|
||||
downloadBar.visibility = View.VISIBLE
|
||||
downloadImage.visibility = View.VISIBLE
|
||||
normalImage.visibility = View.GONE
|
||||
/*setUpButton(
|
||||
card.currentBytes,
|
||||
card.totalBytes,
|
||||
downloadBar,
|
||||
downloadImage,
|
||||
extraInfo,
|
||||
card.child,
|
||||
movieClickCallback
|
||||
)*/
|
||||
binding.downloadHeaderTitle.text = d.name
|
||||
val mbString = formatShortFileSize(itemView.context, card.totalBytes)
|
||||
|
||||
holder.setOnClickListener {
|
||||
movieClickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_PLAY_FILE, card.child))
|
||||
}
|
||||
} else {
|
||||
downloadBar.visibility = View.GONE
|
||||
downloadImage.visibility = View.GONE
|
||||
normalImage.visibility = View.VISIBLE
|
||||
//val isMovie = d.type.isMovieType()
|
||||
if (card.child != null) {
|
||||
//downloadHeaderProgressDownloaded.visibility = View.VISIBLE
|
||||
|
||||
try {
|
||||
extraInfo.text =
|
||||
extraInfo.context.getString(R.string.extra_info_format).format(
|
||||
card.totalDownloads,
|
||||
if (card.totalDownloads == 1) extraInfo.context.getString(R.string.episode) else extraInfo.context.getString(
|
||||
R.string.episodes
|
||||
),
|
||||
mbString
|
||||
// downloadHeaderEpisodeDownload.visibility = View.VISIBLE
|
||||
binding.downloadHeaderGotoChild.visibility = View.GONE
|
||||
|
||||
downloadButton.setDefaultClickListener(card.child, downloadHeaderInfo, movieClickCallback)
|
||||
downloadButton.isVisible = true
|
||||
/*setUpButton(
|
||||
card.currentBytes,
|
||||
card.totalBytes,
|
||||
downloadBar,
|
||||
downloadImage,
|
||||
extraInfo,
|
||||
card.child,
|
||||
movieClickCallback
|
||||
)*/
|
||||
|
||||
episodeHolder.setOnClickListener {
|
||||
movieClickCallback.invoke(
|
||||
DownloadClickEvent(
|
||||
DOWNLOAD_ACTION_PLAY_FILE,
|
||||
card.child
|
||||
)
|
||||
)
|
||||
} catch (t : Throwable) {
|
||||
// you probably formatted incorrectly
|
||||
extraInfo.text = "Error"
|
||||
logError(t)
|
||||
}
|
||||
} else {
|
||||
downloadButton.isVisible = false
|
||||
// downloadHeaderProgressDownloaded.visibility = View.GONE
|
||||
// downloadHeaderEpisodeDownload.visibility = View.GONE
|
||||
binding.downloadHeaderGotoChild.visibility = View.VISIBLE
|
||||
|
||||
try {
|
||||
downloadHeaderInfo.text =
|
||||
downloadHeaderInfo.context.getString(R.string.extra_info_format).format(
|
||||
card.totalDownloads,
|
||||
if (card.totalDownloads == 1) downloadHeaderInfo.context.getString(R.string.episode) else downloadHeaderInfo.context.getString(
|
||||
R.string.episodes
|
||||
),
|
||||
mbString
|
||||
)
|
||||
} catch (t: Throwable) {
|
||||
// you probably formatted incorrectly
|
||||
downloadHeaderInfo.text = "Error"
|
||||
logError(t)
|
||||
}
|
||||
|
||||
|
||||
episodeHolder.setOnClickListener {
|
||||
clickCallback.invoke(DownloadHeaderClickEvent(0, d))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
holder.setOnClickListener {
|
||||
clickCallback.invoke(DownloadHeaderClickEvent(0, d))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reattachDownloadButton() {
|
||||
downloadButton.dispose()
|
||||
val card = localCard
|
||||
if (card?.child != null) {
|
||||
downloadButton.setUpButton(
|
||||
card.currentBytes,
|
||||
card.totalBytes,
|
||||
downloadBar,
|
||||
downloadImage,
|
||||
extraInfo,
|
||||
card.child,
|
||||
movieClickCallback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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}%"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -1,22 +1,23 @@
|
|||
package com.lagradost.cloudstream3.ui.home
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import kotlinx.android.synthetic.main.home_result_grid.view.background_card
|
||||
import kotlinx.android.synthetic.main.home_result_grid_expanded.view.*
|
||||
|
||||
class HomeChildItemAdapter(
|
||||
val cardList: MutableList<SearchResponse>,
|
||||
private val overrideLayout: Int? = null,
|
||||
|
||||
private val nextFocusUp: Int? = null,
|
||||
private val nextFocusDown: Int? = null,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
|
@ -26,16 +27,28 @@ class HomeChildItemAdapter(
|
|||
var hasNext: Boolean = false
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val layout = overrideLayout
|
||||
?: if (parent.context.IsBottomLayout()) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
||||
val expanded = parent.context.IsBottomLayout()
|
||||
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
||||
|
||||
val root = LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||
val binding = HomeResultGridBinding.bind(root)*/
|
||||
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = if (expanded) HomeResultGridExpandedBinding.inflate(
|
||||
inflater,
|
||||
parent,
|
||||
false
|
||||
) else HomeResultGridBinding.inflate(inflater, parent, false)
|
||||
|
||||
|
||||
return CardViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||
binding,
|
||||
clickCallback,
|
||||
itemCount,
|
||||
nextFocusUp,
|
||||
nextFocusDown,
|
||||
isHorizontal
|
||||
isHorizontal,
|
||||
parent.isRtl()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -69,14 +82,15 @@ class HomeChildItemAdapter(
|
|||
|
||||
class CardViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
val binding: ViewBinding,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
var itemCount: Int,
|
||||
private val nextFocusUp: Int? = null,
|
||||
private val nextFocusDown: Int? = null,
|
||||
private val isHorizontal: Boolean = false
|
||||
private val isHorizontal: Boolean = false,
|
||||
private val isRtl : Boolean
|
||||
) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(card: SearchResponse, position: Int) {
|
||||
|
||||
|
@ -87,26 +101,71 @@ class HomeChildItemAdapter(
|
|||
else -> null
|
||||
}
|
||||
|
||||
(itemView.image_holder ?: itemView.background_card)?.apply {
|
||||
val min = 114.toPx
|
||||
val max = 180.toPx
|
||||
|
||||
layoutParams =
|
||||
layoutParams.apply {
|
||||
width = if (!isHorizontal) {
|
||||
min
|
||||
} else {
|
||||
max
|
||||
}
|
||||
height = if (!isHorizontal) {
|
||||
max
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
if (position == 0) { // to fix tv
|
||||
if (isRtl) {
|
||||
itemView.nextFocusRightId = R.id.nav_rail_view
|
||||
itemView.nextFocusLeftId = -1
|
||||
}
|
||||
else {
|
||||
itemView.nextFocusLeftId = R.id.nav_rail_view
|
||||
itemView.nextFocusRightId = -1
|
||||
}
|
||||
} else {
|
||||
itemView.nextFocusRightId = -1
|
||||
itemView.nextFocusLeftId = -1
|
||||
}
|
||||
|
||||
|
||||
when (binding) {
|
||||
is HomeResultGridBinding -> {
|
||||
binding.backgroundCard.apply {
|
||||
val min = 114.toPx
|
||||
val max = 180.toPx
|
||||
|
||||
layoutParams =
|
||||
layoutParams.apply {
|
||||
width = if (!isHorizontal) {
|
||||
min
|
||||
} else {
|
||||
max
|
||||
}
|
||||
height = if (!isHorizontal) {
|
||||
max
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
is HomeResultGridExpandedBinding -> {
|
||||
binding.backgroundCard.apply {
|
||||
val min = 114.toPx
|
||||
val max = 180.toPx
|
||||
|
||||
layoutParams =
|
||||
layoutParams.apply {
|
||||
width = if (!isHorizontal) {
|
||||
min
|
||||
} else {
|
||||
max
|
||||
}
|
||||
height = if (!isHorizontal) {
|
||||
max
|
||||
} else {
|
||||
min
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (position == 0) { // to fix tv
|
||||
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchResultBuilder.bind(
|
||||
clickCallback,
|
||||
card,
|
||||
|
@ -118,9 +177,6 @@ class HomeChildItemAdapter(
|
|||
)
|
||||
itemView.tag = position
|
||||
|
||||
if (position == 0) { // to fix tv
|
||||
itemView.background_card?.nextFocusLeftId = R.id.nav_rail_view
|
||||
}
|
||||
//val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f)
|
||||
//ani.fillAfter = true
|
||||
//ani.duration = 200
|
||||
|
|
|
@ -31,17 +31,23 @@ import com.lagradost.cloudstream3.APIHolder.apis
|
|||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent
|
||||
import com.lagradost.cloudstream3.databinding.FragmentHomeBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding
|
||||
import com.lagradost.cloudstream3.databinding.TvtypesChipsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
|
||||
import com.lagradost.cloudstream3.ui.AutofitRecyclerView
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.ui.search.*
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
|
@ -64,24 +70,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
import kotlinx.android.synthetic.main.activity_main_tv.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_api_fab
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_change_api_loading
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_loading
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_loading_error
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_loading_shimmer
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_loading_statusbar
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_master_recycler
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_reload_connection_open_in_browser
|
||||
import kotlinx.android.synthetic.main.fragment_home.home_reload_connectionerror
|
||||
import kotlinx.android.synthetic.main.fragment_home.result_error_text
|
||||
import kotlinx.android.synthetic.main.fragment_home_tv.*
|
||||
import kotlinx.android.synthetic.main.fragment_result.*
|
||||
import kotlinx.android.synthetic.main.fragment_search.*
|
||||
import kotlinx.android.synthetic.main.home_episodes_expanded.*
|
||||
import kotlinx.android.synthetic.main.tvtypes_chips.*
|
||||
import kotlinx.android.synthetic.main.tvtypes_chips.view.*
|
||||
|
||||
import java.util.*
|
||||
|
||||
|
||||
|
@ -125,22 +114,26 @@ class HomeFragment : Fragment() {
|
|||
expand: HomeViewModel.ExpandableHomepageList,
|
||||
deleteCallback: (() -> Unit)? = null,
|
||||
expandCallback: (suspend (String) -> HomeViewModel.ExpandableHomepageList?)? = null,
|
||||
dismissCallback : (() -> Unit),
|
||||
dismissCallback: (() -> Unit),
|
||||
): BottomSheetDialog {
|
||||
val context = this
|
||||
val bottomSheetDialogBuilder = BottomSheetDialog(context)
|
||||
|
||||
bottomSheetDialogBuilder.setContentView(R.layout.home_episodes_expanded)
|
||||
val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
|
||||
val binding: HomeEpisodesExpandedBinding = HomeEpisodesExpandedBinding.inflate(
|
||||
bottomSheetDialogBuilder.layoutInflater,
|
||||
null,
|
||||
false
|
||||
)
|
||||
bottomSheetDialogBuilder.setContentView(binding.root)
|
||||
//val title = bottomSheetDialogBuilder.findViewById<TextView>(R.id.home_expanded_text)!!
|
||||
|
||||
//title.findViewTreeLifecycleOwner().lifecycle.addObserver()
|
||||
|
||||
val item = expand.list
|
||||
title.text = item.name
|
||||
val recycle =
|
||||
bottomSheetDialogBuilder.findViewById<AutofitRecyclerView>(R.id.home_expanded_recycler)!!
|
||||
val titleHolder =
|
||||
bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!!
|
||||
binding.homeExpandedText.text = item.name
|
||||
// val recycle =
|
||||
// bottomSheetDialogBuilder.findViewById<AutofitRecyclerView>(R.id.home_expanded_recycler)!!
|
||||
//val titleHolder =
|
||||
// bottomSheetDialogBuilder.findViewById<FrameLayout>(R.id.home_expanded_drag_down)!!
|
||||
|
||||
// main {
|
||||
//(bottomSheetDialogBuilder.ownerActivity as androidx.fragment.app.FragmentActivity?)?.supportFragmentManager?.fragments?.lastOrNull()?.viewLifecycleOwner?.apply {
|
||||
|
@ -159,10 +152,10 @@ class HomeFragment : Fragment() {
|
|||
// })
|
||||
//}
|
||||
// }
|
||||
val delete = bottomSheetDialogBuilder.home_expanded_delete
|
||||
delete.isGone = deleteCallback == null
|
||||
//val delete = bottomSheetDialogBuilder.home_expanded_delete
|
||||
binding.homeExpandedDelete.isGone = deleteCallback == null
|
||||
if (deleteCallback != null) {
|
||||
delete.setOnClickListener {
|
||||
binding.homeExpandedDelete.setOnClickListener {
|
||||
try {
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(context)
|
||||
val dialogClickListener =
|
||||
|
@ -172,6 +165,7 @@ class HomeFragment : Fragment() {
|
|||
deleteCallback.invoke()
|
||||
bottomSheetDialogBuilder.dismissSafe(this)
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEGATIVE -> {}
|
||||
}
|
||||
}
|
||||
|
@ -191,26 +185,27 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
titleHolder.setOnClickListener {
|
||||
binding.homeExpandedDragDown.setOnClickListener {
|
||||
bottomSheetDialogBuilder.dismissSafe(this)
|
||||
}
|
||||
|
||||
|
||||
// Span settings
|
||||
recycle.spanCount = currentSpan
|
||||
binding.homeExpandedRecycler.spanCount = currentSpan
|
||||
|
||||
recycle.adapter = SearchAdapter(item.list.toMutableList(), recycle) { callback ->
|
||||
handleSearchClickCallback(this, callback)
|
||||
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
|
||||
bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later
|
||||
//bottomSheetDialogBuilder.dismissSafe(this)
|
||||
binding.homeExpandedRecycler.adapter =
|
||||
SearchAdapter(item.list.toMutableList(), binding.homeExpandedRecycler) { callback ->
|
||||
handleSearchClickCallback(callback)
|
||||
if (callback.action == SEARCH_ACTION_LOAD || callback.action == SEARCH_ACTION_PLAY_FILE) {
|
||||
bottomSheetDialogBuilder.ownHide() // we hide here because we want to resume it later
|
||||
//bottomSheetDialogBuilder.dismissSafe(this)
|
||||
}
|
||||
}.apply {
|
||||
hasNext = expand.hasNext
|
||||
}
|
||||
}.apply {
|
||||
hasNext = expand.hasNext
|
||||
}
|
||||
|
||||
recycle.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
binding.homeExpandedRecycler.addOnScrollListener(object :
|
||||
RecyclerView.OnScrollListener() {
|
||||
var expandCount = 0
|
||||
val name = expand.list.name
|
||||
|
||||
|
@ -238,7 +233,7 @@ class HomeFragment : Fragment() {
|
|||
})
|
||||
|
||||
val spanListener = { span: Int ->
|
||||
recycle.spanCount = span
|
||||
binding.homeExpandedRecycler.spanCount = span
|
||||
//(recycle.adapter as SearchAdapter).notifyDataSetChanged()
|
||||
}
|
||||
|
||||
|
@ -280,19 +275,19 @@ class HomeFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
|
||||
private fun getPairList(header: ChipGroup) = getPairList(
|
||||
header.home_select_anime,
|
||||
header.home_select_cartoons,
|
||||
header.home_select_tv_series,
|
||||
header.home_select_documentaries,
|
||||
header.home_select_movies,
|
||||
header.home_select_asian,
|
||||
header.home_select_livestreams,
|
||||
header.home_select_nsfw,
|
||||
header.home_select_others
|
||||
private fun getPairList(header: TvtypesChipsBinding) = getPairList(
|
||||
header.homeSelectAnime,
|
||||
header.homeSelectCartoons,
|
||||
header.homeSelectTvSeries,
|
||||
header.homeSelectDocumentaries,
|
||||
header.homeSelectMovies,
|
||||
header.homeSelectAsian,
|
||||
header.homeSelectLivestreams,
|
||||
header.homeSelectNsfw,
|
||||
header.homeSelectOthers
|
||||
)
|
||||
|
||||
fun validateChips(header: ChipGroup?, validTypes: List<TvType>) {
|
||||
fun validateChips(header: TvtypesChipsBinding?, validTypes: List<TvType>) {
|
||||
if (header == null) return
|
||||
val pairList = getPairList(header)
|
||||
for ((button, types) in pairList) {
|
||||
|
@ -301,7 +296,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateChips(header: ChipGroup?, selectedTypes: List<TvType>) {
|
||||
fun updateChips(header: TvtypesChipsBinding?, selectedTypes: List<TvType>) {
|
||||
if (header == null) return
|
||||
val pairList = getPairList(header)
|
||||
for ((button, types) in pairList) {
|
||||
|
@ -311,7 +306,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
fun bindChips(
|
||||
header: ChipGroup?,
|
||||
header: TvtypesChipsBinding?,
|
||||
selectedTypes: List<TvType>,
|
||||
validTypes: List<TvType>,
|
||||
callback: (List<TvType>) -> Unit
|
||||
|
@ -344,7 +339,13 @@ class HomeFragment : Fragment() {
|
|||
BottomSheetDialog(this)
|
||||
|
||||
builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
builder.setContentView(R.layout.home_select_mainpage)
|
||||
val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate(
|
||||
builder.layoutInflater,
|
||||
null,
|
||||
false
|
||||
)
|
||||
|
||||
builder.setContentView(binding.root)
|
||||
builder.show()
|
||||
builder.let { dialog ->
|
||||
val isMultiLang = getApiProviderLangSettings().let { set ->
|
||||
|
@ -360,14 +361,11 @@ class HomeFragment : Fragment() {
|
|||
?.toMutableList()
|
||||
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||
|
||||
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
|
||||
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
|
||||
|
||||
cancelBtt?.setOnClickListener {
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
|
||||
applyBtt?.setOnClickListener {
|
||||
binding.applyBtt.setOnClickListener {
|
||||
if (currentApiName != selectedApiName) {
|
||||
currentApiName?.let(callback)
|
||||
}
|
||||
|
@ -408,7 +406,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
bindChips(
|
||||
dialog.home_select_group,
|
||||
binding.tvtypesChipsScroll.tvtypesChips,
|
||||
preSelectedTypes,
|
||||
validAPIs.flatMap { it.supportedTypes }.distinct()
|
||||
) { list ->
|
||||
|
@ -423,6 +421,9 @@ class HomeFragment : Fragment() {
|
|||
|
||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||
|
||||
var binding: FragmentHomeBinding? = null
|
||||
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
|
@ -430,14 +431,25 @@ class HomeFragment : Fragment() {
|
|||
): View? {
|
||||
//homeViewModel =
|
||||
// ViewModelProvider(this).get(HomeViewModel::class.java)
|
||||
|
||||
bottomSheetDialog?.ownShow()
|
||||
val layout =
|
||||
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home
|
||||
return inflater.inflate(layout, container, false)
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
binding = try {
|
||||
FragmentHomeBinding.bind(root)
|
||||
} catch (t: Throwable) {
|
||||
showToast(txt(R.string.unable_to_inflate, t.message ?: ""), Toast.LENGTH_LONG)
|
||||
logError(t)
|
||||
null
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
bottomSheetDialog?.ownHide()
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
@ -450,7 +462,7 @@ class HomeFragment : Fragment() {
|
|||
|
||||
private val apiChangeClickListener = View.OnClickListener { view ->
|
||||
view.context.selectHomepage(currentApiName) { api ->
|
||||
homeViewModel.loadAndCancel(api)
|
||||
homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true)
|
||||
}
|
||||
/*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf()
|
||||
|
||||
|
@ -467,196 +479,149 @@ class HomeFragment : Fragment() {
|
|||
fixGrid()
|
||||
}
|
||||
|
||||
fun bookmarksUpdated(_data : Boolean) {
|
||||
reloadStored()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
reloadStored()
|
||||
bookmarksUpdatedEvent += ::bookmarksUpdated
|
||||
afterPluginsLoadedEvent += ::afterPluginsLoaded
|
||||
mainPluginsLoadedEvent += ::afterMainPluginsLoaded
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
bookmarksUpdatedEvent -= ::bookmarksUpdated
|
||||
afterPluginsLoadedEvent -= ::afterPluginsLoaded
|
||||
mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun reloadStored() {
|
||||
homeViewModel.loadResumeWatching()
|
||||
val list = EnumSet.noneOf(WatchType::class.java)
|
||||
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
|
||||
list.addAll(it)
|
||||
}
|
||||
homeViewModel.loadStoredData(list)
|
||||
}
|
||||
|
||||
private fun afterMainPluginsLoaded(unused: Boolean = false) {
|
||||
loadHomePage(false)
|
||||
}
|
||||
|
||||
private fun afterPluginsLoaded(forceReload: Boolean) {
|
||||
loadHomePage(forceReload)
|
||||
}
|
||||
|
||||
private fun loadHomePage(forceReload: Boolean) {
|
||||
val apiName = context?.getKey<String>(USER_SELECTED_HOMEPAGE_API)
|
||||
|
||||
if (homeViewModel.apiName.value != apiName || apiName == null || forceReload) {
|
||||
//println("Caught home: " + homeViewModel.apiName.value + " at " + apiName)
|
||||
homeViewModel.loadAndCancel(apiName, forceReload)
|
||||
}
|
||||
}
|
||||
|
||||
private fun homeHandleSearch(callback: SearchClickCallback) {
|
||||
if (callback.action == SEARCH_ACTION_FOCUSED) {
|
||||
//focusCallback(callback.card)
|
||||
} else {
|
||||
handleSearchClickCallback(activity, callback)
|
||||
}
|
||||
}
|
||||
|
||||
private var currentApiName: String? = null
|
||||
private var toggleRandomButton = false
|
||||
|
||||
private var bottomSheetDialog: BottomSheetDialog? = null
|
||||
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
fixGrid()
|
||||
|
||||
home_change_api_loading?.setOnClickListener(apiChangeClickListener)
|
||||
home_api_fab?.setOnClickListener(apiChangeClickListener)
|
||||
home_random?.setOnClickListener {
|
||||
if (listHomepageItems.isNotEmpty()) {
|
||||
activity.loadSearchResult(listHomepageItems.random())
|
||||
binding?.apply {
|
||||
homeChangeApiLoading.setOnClickListener(apiChangeClickListener)
|
||||
//homeChangeApiLoading.setOnClickListener(apiChangeClickListener)
|
||||
homeApiFab.setOnClickListener(apiChangeClickListener)
|
||||
homeRandom.setOnClickListener {
|
||||
if (listHomepageItems.isNotEmpty()) {
|
||||
activity.loadSearchResult(listHomepageItems.random())
|
||||
}
|
||||
}
|
||||
|
||||
homeMasterRecycler.adapter =
|
||||
HomeParentItemAdapterPreview(
|
||||
mutableListOf(),
|
||||
homeViewModel
|
||||
)
|
||||
fixPaddingStatusbar(homeLoadingStatusbar)
|
||||
|
||||
if (isTvSettings()) {
|
||||
homeApiFab.isVisible = false
|
||||
if (isTrueTvSettings()) {
|
||||
homeChangeApiLoading.isVisible = true
|
||||
homeChangeApiLoading.isFocusable = true
|
||||
homeChangeApiLoading.isFocusableInTouchMode = true
|
||||
}
|
||||
// home_bookmark_select?.isFocusable = true
|
||||
// home_bookmark_select?.isFocusableInTouchMode = true
|
||||
} else {
|
||||
homeApiFab.isVisible = true
|
||||
homeChangeApiLoading.isVisible = false
|
||||
}
|
||||
|
||||
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0) { //check for scroll down
|
||||
homeApiFab.shrink() // hide
|
||||
homeRandom.shrink()
|
||||
} else if (dy < -5) {
|
||||
if (!isTvSettings()) {
|
||||
homeApiFab.extend() // show
|
||||
homeRandom.extend()
|
||||
}
|
||||
}
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//Load value for toggling Random button. Hide at startup
|
||||
context?.let {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(it)
|
||||
toggleRandomButton =
|
||||
settingsManager.getBoolean(getString(R.string.random_button_key), false)
|
||||
home_random?.visibility = View.GONE
|
||||
}
|
||||
|
||||
observe(homeViewModel.preview) { preview ->
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setPreviewData(
|
||||
preview
|
||||
)
|
||||
settingsManager.getBoolean(
|
||||
getString(R.string.random_button_key),
|
||||
false
|
||||
) && !isTvSettings()
|
||||
binding?.homeRandom?.visibility = View.GONE
|
||||
}
|
||||
|
||||
observe(homeViewModel.apiName) { apiName ->
|
||||
currentApiName = apiName
|
||||
home_api_fab?.text = apiName
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setApiName(
|
||||
apiName
|
||||
)
|
||||
binding?.homeApiFab?.text = apiName
|
||||
}
|
||||
|
||||
observe(homeViewModel.page) { data ->
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
home_loading_shimmer?.stopShimmer()
|
||||
binding?.apply {
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
homeLoadingShimmer.stopShimmer()
|
||||
|
||||
val d = data.value
|
||||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||
listHomepageItems.clear()
|
||||
val d = data.value
|
||||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||
listHomepageItems.clear()
|
||||
|
||||
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(
|
||||
d.values.toMutableList(),
|
||||
home_master_recycler
|
||||
)
|
||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(
|
||||
d.values.toMutableList(),
|
||||
homeMasterRecycler
|
||||
)
|
||||
|
||||
home_loading?.isVisible = false
|
||||
home_loading_error?.isVisible = false
|
||||
home_master_recycler?.isVisible = true
|
||||
//home_loaded?.isVisible = true
|
||||
if (toggleRandomButton) {
|
||||
//Flatten list
|
||||
d.values.forEach { dlist ->
|
||||
mutableListOfResponse.addAll(dlist.list.list)
|
||||
homeLoading.isVisible = false
|
||||
homeLoadingError.isVisible = false
|
||||
homeMasterRecycler.isVisible = true
|
||||
//home_loaded?.isVisible = true
|
||||
if (toggleRandomButton) {
|
||||
//Flatten list
|
||||
d.values.forEach { dlist ->
|
||||
mutableListOfResponse.addAll(dlist.list.list)
|
||||
}
|
||||
listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url })
|
||||
|
||||
homeRandom.isVisible = listHomepageItems.isNotEmpty()
|
||||
} else {
|
||||
homeRandom.isGone = true
|
||||
}
|
||||
listHomepageItems.addAll(mutableListOfResponse.distinctBy { it.url })
|
||||
home_random?.isVisible = listHomepageItems.isNotEmpty()
|
||||
} else {
|
||||
home_random?.isGone = true
|
||||
}
|
||||
}
|
||||
is Resource.Failure -> {
|
||||
home_loading_shimmer?.stopShimmer()
|
||||
|
||||
result_error_text.text = data.errorString
|
||||
is Resource.Failure -> {
|
||||
homeLoadingShimmer.stopShimmer()
|
||||
resultErrorText.text = data.errorString
|
||||
homeReloadConnectionerror.setOnClickListener(apiChangeClickListener)
|
||||
homeReloadConnectionOpenInBrowser.setOnClickListener { view ->
|
||||
val validAPIs = apis//.filter { api -> api.hasMainPage }
|
||||
|
||||
home_reload_connectionerror.setOnClickListener(apiChangeClickListener)
|
||||
|
||||
home_reload_connection_open_in_browser.setOnClickListener { view ->
|
||||
val validAPIs = apis//.filter { api -> api.hasMainPage }
|
||||
|
||||
view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api ->
|
||||
Pair(
|
||||
index,
|
||||
api.name
|
||||
)
|
||||
}) {
|
||||
try {
|
||||
val i = Intent(Intent.ACTION_VIEW)
|
||||
i.data = Uri.parse(validAPIs[itemId].mainUrl)
|
||||
startActivity(i)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
view.popupMenuNoIconsAndNoStringRes(validAPIs.mapIndexed { index, api ->
|
||||
Pair(
|
||||
index,
|
||||
api.name
|
||||
)
|
||||
}) {
|
||||
try {
|
||||
val i = Intent(Intent.ACTION_VIEW)
|
||||
i.data = Uri.parse(validAPIs[itemId].mainUrl)
|
||||
startActivity(i)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
homeLoading.isVisible = false
|
||||
homeLoadingError.isVisible = true
|
||||
homeMasterRecycler.isVisible = false
|
||||
//home_loaded?.isVisible = false
|
||||
}
|
||||
|
||||
home_loading?.isVisible = false
|
||||
home_loading_error?.isVisible = true
|
||||
home_master_recycler?.isVisible = false
|
||||
//home_loaded?.isVisible = false
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
(home_master_recycler?.adapter as? ParentItemAdapter)?.updateList(listOf())
|
||||
home_loading_shimmer?.startShimmer()
|
||||
home_loading?.isVisible = true
|
||||
home_loading_error?.isVisible = false
|
||||
home_master_recycler?.isVisible = false
|
||||
//home_loaded?.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
observe(homeViewModel.availableWatchStatusTypes) { availableWatchStatusTypes ->
|
||||
context?.setKey(
|
||||
HOME_BOOKMARK_VALUE_LIST,
|
||||
availableWatchStatusTypes.first.map { it.internalId }.toIntArray()
|
||||
)
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setAvailableWatchStatusTypes(
|
||||
availableWatchStatusTypes
|
||||
)
|
||||
}
|
||||
|
||||
observe(homeViewModel.bookmarks) { data ->
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setBookmarkData(
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
observe(homeViewModel.resumeWatching) { resumeWatching ->
|
||||
(home_master_recycler?.adapter as? HomeParentItemAdapterPreview?)?.setResumeWatchingData(
|
||||
resumeWatching
|
||||
)
|
||||
if (isTrueTvSettings()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ioSafe {
|
||||
activity?.addProgramsToContinueWatching(resumeWatching.mapNotNull { it as? DataStoreHelper.ResumeWatchingResult })
|
||||
is Resource.Loading -> {
|
||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf())
|
||||
homeLoadingShimmer.startShimmer()
|
||||
homeLoading.isVisible = true
|
||||
homeLoadingError.isVisible = false
|
||||
homeMasterRecycler.isVisible = false
|
||||
//home_loaded?.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -665,72 +630,35 @@ class HomeFragment : Fragment() {
|
|||
|
||||
//context?.fixPaddingStatusbarView(home_statusbar)
|
||||
//context?.fixPaddingStatusbar(home_padding)
|
||||
context?.fixPaddingStatusbar(home_loading_statusbar)
|
||||
|
||||
home_master_recycler?.adapter =
|
||||
HomeParentItemAdapterPreview(mutableListOf(), { callback ->
|
||||
homeHandleSearch(callback)
|
||||
}, { item ->
|
||||
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = {
|
||||
homeViewModel.expandAndReturn(it)
|
||||
}, dismissCallback = {
|
||||
bottomSheetDialog = null
|
||||
})
|
||||
}, { name ->
|
||||
homeViewModel.expand(name)
|
||||
}, { load ->
|
||||
activity?.loadResult(load.response.url, load.response.apiName, load.action)
|
||||
}, {
|
||||
homeViewModel.loadMoreHomeScrollResponses()
|
||||
}, {
|
||||
apiChangeClickListener.onClick(it)
|
||||
}, reloadStored = {
|
||||
reloadStored()
|
||||
}, loadStoredData = {
|
||||
homeViewModel.loadStoredData(it)
|
||||
}, { (isQuickSearch, text) ->
|
||||
if (!isQuickSearch) {
|
||||
QuickSearchFragment.pushSearch(
|
||||
activity,
|
||||
text,
|
||||
currentApiName?.let { arrayOf(it) })
|
||||
}
|
||||
})
|
||||
|
||||
reloadStored()
|
||||
loadHomePage(false)
|
||||
home_master_recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0) { //check for scroll down
|
||||
home_api_fab?.shrink() // hide
|
||||
home_random?.shrink()
|
||||
} else if (dy < -5) {
|
||||
if (!isTvSettings()) {
|
||||
home_api_fab?.extend() // show
|
||||
home_random?.extend()
|
||||
}
|
||||
}
|
||||
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
observeNullable(homeViewModel.popup) { item ->
|
||||
if (item == null) {
|
||||
bottomSheetDialog?.dismissSafe()
|
||||
bottomSheetDialog = null
|
||||
return@observeNullable
|
||||
}
|
||||
})
|
||||
|
||||
// don't recreate
|
||||
if (bottomSheetDialog != null) {
|
||||
return@observeNullable
|
||||
}
|
||||
|
||||
bottomSheetDialog = activity?.loadHomepageList(item, expandCallback = {
|
||||
homeViewModel.expandAndReturn(it)
|
||||
}, dismissCallback = {
|
||||
homeViewModel.popup(null)
|
||||
bottomSheetDialog = null
|
||||
})
|
||||
}
|
||||
|
||||
homeViewModel.reloadStored()
|
||||
homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false)
|
||||
//loadHomePage(false)
|
||||
|
||||
// nice profile pic on homepage
|
||||
//home_profile_picture_holder?.isVisible = false
|
||||
// just in case
|
||||
if (isTvSettings()) {
|
||||
home_api_fab?.isVisible = false
|
||||
if (isTrueTvSettings()) {
|
||||
home_change_api_loading?.isVisible = true
|
||||
home_change_api_loading?.isFocusable = true
|
||||
home_change_api_loading?.isFocusableInTouchMode = true
|
||||
}
|
||||
// home_bookmark_select?.isFocusable = true
|
||||
// home_bookmark_select?.isFocusableInTouchMode = true
|
||||
} else {
|
||||
home_api_fab?.isVisible = true
|
||||
home_change_api_loading?.isVisible = false
|
||||
}
|
||||
|
||||
//TODO READD THIS
|
||||
/*for (syncApi in OAuth2Apis) {
|
||||
val login = syncApi.loginInfo()
|
||||
|
|
|
@ -3,50 +3,19 @@ package com.lagradost.cloudstream3.ui.home
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.ChangeBounds
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipDrawable
|
||||
import com.lagradost.cloudstream3.APIHolder.getId
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.result.LinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
|
||||
import com.lagradost.cloudstream3.ui.result.START_ACTION_RESUME_LATEST
|
||||
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
|
||||
import kotlinx.android.synthetic.main.activity_main_tv.*
|
||||
import kotlinx.android.synthetic.main.activity_main_tv.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.fragment_home.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.home_preview_viewpager
|
||||
import kotlinx.android.synthetic.main.homepage_parent.view.*
|
||||
|
||||
class LoadClickCallback(
|
||||
val action: Int = 0,
|
||||
|
@ -57,17 +26,23 @@ class LoadClickCallback(
|
|||
|
||||
open class ParentItemAdapter(
|
||||
private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
||||
//private val viewModel: HomeViewModel,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
||||
private val expandCallback: ((String) -> Unit)? = null,
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
val root = LayoutInflater.from(parent.context).inflate(
|
||||
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
|
||||
val binding = HomepageParentBinding.bind(root)
|
||||
|
||||
return ParentViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(
|
||||
if (isTvSettings()) R.layout.homepage_parent_tv else R.layout.homepage_parent,
|
||||
parent,
|
||||
false
|
||||
),
|
||||
binding,
|
||||
clickCallback,
|
||||
moreInfoClickCallback,
|
||||
expandCallback
|
||||
|
@ -178,14 +153,15 @@ open class ParentItemAdapter(
|
|||
|
||||
class ParentViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
val binding: HomepageParentBinding,
|
||||
// val viewModel: HomeViewModel,
|
||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
||||
private val expandCallback: ((String) -> Unit)? = null,
|
||||
) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
val title: TextView = itemView.home_child_more_info
|
||||
private val recyclerView: RecyclerView = itemView.home_child_recyclerview
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
val title: TextView = binding.homeChildMoreInfo
|
||||
private val recyclerView: RecyclerView = binding.homeChildRecyclerview
|
||||
|
||||
fun update(expand: HomeViewModel.ExpandableHomepageList) {
|
||||
val info = expand.list
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -2,24 +2,18 @@ package com.lagradost.cloudstream3.ui.home
|
|||
|
||||
import android.content.res.Configuration
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.core.view.isGone
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.*
|
||||
import kotlinx.android.synthetic.main.fragment_home_head_tv.view.*
|
||||
import kotlinx.android.synthetic.main.home_scroll_view.view.*
|
||||
|
||||
|
||||
class HomeScrollAdapter(
|
||||
@LayoutRes val layout: Int = R.layout.home_scroll_view,
|
||||
private val forceHorizontalPosters: Boolean? = null
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private var items: MutableList<LoadResponse> = mutableListOf()
|
||||
var hasMoreItems: Boolean = false
|
||||
|
||||
|
@ -45,9 +39,16 @@ class HomeScrollAdapter(
|
|||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val binding = if (isTvSettings()) {
|
||||
HomeScrollViewTvBinding.inflate(inflater, parent, false)
|
||||
} else {
|
||||
HomeScrollViewBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
return CardViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false),
|
||||
forceHorizontalPosters
|
||||
binding,
|
||||
//forceHorizontalPosters
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -61,22 +62,32 @@ class HomeScrollAdapter(
|
|||
|
||||
class CardViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
private val forceHorizontalPosters: Boolean? = null
|
||||
val binding: ViewBinding,
|
||||
//private val forceHorizontalPosters: Boolean? = null
|
||||
) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(card: LoadResponse) {
|
||||
card.apply {
|
||||
val isHorizontal =
|
||||
(forceHorizontalPosters == true) || ((forceHorizontalPosters != false) && itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE)
|
||||
val isHorizontal =
|
||||
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
val posterUrl = if (isHorizontal) backgroundPosterUrl ?: posterUrl else posterUrl
|
||||
?: backgroundPosterUrl
|
||||
itemView.home_scroll_preview_tags?.text = tags?.joinToString(" • ") ?: ""
|
||||
itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty()
|
||||
itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders)
|
||||
itemView.home_scroll_preview_title?.text = name
|
||||
val posterUrl =
|
||||
if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
|
||||
?: card.backgroundPosterUrl
|
||||
|
||||
when (binding) {
|
||||
is HomeScrollViewBinding -> {
|
||||
binding.homeScrollPreview.setImage(posterUrl)
|
||||
binding.homeScrollPreviewTags.apply {
|
||||
text = card.tags?.joinToString(" • ") ?: ""
|
||||
isGone = card.tags.isNullOrEmpty()
|
||||
}
|
||||
binding.homeScrollPreviewTitle.text = card.name
|
||||
}
|
||||
|
||||
is HomeScrollViewTvBinding -> {
|
||||
binding.homeScrollPreview.setImage(posterUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package com.lagradost.cloudstream3.ui.home
|
||||
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
|
||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||
|
@ -13,11 +13,30 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.debugWarning
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
|
@ -32,7 +51,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
import kotlin.collections.set
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
|
@ -72,7 +91,7 @@ class HomeViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
private var repo: APIRepository? = null
|
||||
var repo: APIRepository? = null
|
||||
|
||||
private val _apiName = MutableLiveData<String>()
|
||||
val apiName: LiveData<String> = _apiName
|
||||
|
@ -83,7 +102,7 @@ class HomeViewModel : ViewModel() {
|
|||
private var currentShuffledList: List<SearchResponse> = listOf()
|
||||
|
||||
private fun autoloadRepo(): APIRepository {
|
||||
return APIRepository(apis.first { it.hasMainPage })
|
||||
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }})
|
||||
}
|
||||
|
||||
private val _availableWatchStatusTypes =
|
||||
|
@ -101,8 +120,14 @@ class HomeViewModel : ViewModel() {
|
|||
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
|
||||
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
|
||||
|
||||
fun loadResumeWatching() = viewModelScope.launchSafe {
|
||||
private fun loadResumeWatching() = viewModelScope.launchSafe {
|
||||
val resumeWatchingResult = getResumeWatching()
|
||||
if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ioSafe {
|
||||
// this WILL crash on non tvs, so keep this inside a try catch
|
||||
activity?.addProgramsToContinueWatching(resumeWatchingResult)
|
||||
}
|
||||
}
|
||||
resumeWatchingResult?.let {
|
||||
_resumeWatching.postValue(it)
|
||||
}
|
||||
|
@ -128,6 +153,10 @@ class HomeViewModel : ViewModel() {
|
|||
currentWatchTypes.remove(WatchType.NONE)
|
||||
|
||||
if (currentWatchTypes.size <= 0) {
|
||||
setKey(
|
||||
HOME_BOOKMARK_VALUE_LIST,
|
||||
intArrayOf()
|
||||
)
|
||||
_availableWatchStatusTypes.postValue(setOf<WatchType>() to setOf())
|
||||
_bookmarks.postValue(Pair(false, ArrayList()))
|
||||
return@launchSafe
|
||||
|
@ -135,7 +164,10 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first())
|
||||
//if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first())
|
||||
|
||||
setKey(
|
||||
HOME_BOOKMARK_VALUE_LIST,
|
||||
watchPrefNotNull.map { it.internalId }.toIntArray()
|
||||
)
|
||||
_availableWatchStatusTypes.postValue(
|
||||
Pair(
|
||||
watchPrefNotNull,
|
||||
|
@ -152,8 +184,10 @@ class HomeViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private var onGoingLoad: Job? = null
|
||||
private fun loadAndCancel(api: MainAPI?) {
|
||||
private var isCurrentlyLoadingName : String? = null
|
||||
private fun loadAndCancel(api: MainAPI) {
|
||||
onGoingLoad?.cancel()
|
||||
isCurrentlyLoadingName = api.name
|
||||
onGoingLoad = load(api)
|
||||
}
|
||||
|
||||
|
@ -255,12 +289,12 @@ class HomeViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun load(api: MainAPI?) = ioSafe {
|
||||
repo = if (api != null) {
|
||||
private fun load(api: MainAPI) : Job = ioSafe {
|
||||
repo = //if (api != null) {
|
||||
APIRepository(api)
|
||||
} else {
|
||||
autoloadRepo()
|
||||
}
|
||||
//} else {
|
||||
// autoloadRepo()
|
||||
//}
|
||||
|
||||
_apiName.postValue(repo?.name)
|
||||
_randomItems.postValue(listOf())
|
||||
|
@ -274,6 +308,7 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
_page.postValue(Resource.Loading())
|
||||
_preview.postValue(Resource.Loading())
|
||||
// cancel the current preview expand as that is no longer relevant
|
||||
addJob?.cancel()
|
||||
|
||||
when (val data = repo?.getMainPage(1, null)) {
|
||||
|
@ -337,41 +372,126 @@ class HomeViewModel : ViewModel() {
|
|||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
is Resource.Failure -> {
|
||||
_page.postValue(data!!)
|
||||
_preview.postValue(data!!)
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
isCurrentlyLoadingName = null
|
||||
}
|
||||
|
||||
fun click(callback: SearchClickCallback) {
|
||||
if (callback.action == SEARCH_ACTION_FOCUSED) {
|
||||
//focusCallback(callback.card)
|
||||
} else {
|
||||
SearchHelper.handleSearchClickCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) =
|
||||
viewModelScope.launchSafe {
|
||||
|
||||
private val _popup = MutableLiveData<ExpandableHomepageList?>(null)
|
||||
val popup: LiveData<ExpandableHomepageList?> = _popup
|
||||
|
||||
fun popup(list: ExpandableHomepageList?) {
|
||||
_popup.postValue(list)
|
||||
}
|
||||
|
||||
private fun bookmarksUpdated(unused: Boolean) {
|
||||
reloadStored()
|
||||
}
|
||||
|
||||
private fun afterPluginsLoaded(forceReload: Boolean) {
|
||||
loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload)
|
||||
}
|
||||
|
||||
private fun afterMainPluginsLoaded(unused: Boolean = false) {
|
||||
loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false)
|
||||
}
|
||||
|
||||
init {
|
||||
MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated
|
||||
MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded
|
||||
MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated
|
||||
MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded
|
||||
MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun queryTextSubmit(query: String) {
|
||||
QuickSearchFragment.pushSearch(
|
||||
query,
|
||||
repo?.name?.let { arrayOf(it) })
|
||||
}
|
||||
|
||||
fun queryTextChange(newText: String) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
fun reloadStored() {
|
||||
loadResumeWatching()
|
||||
val list = EnumSet.noneOf(WatchType::class.java)
|
||||
getKey<IntArray>(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let {
|
||||
list.addAll(it)
|
||||
}
|
||||
loadStoredData(list)
|
||||
}
|
||||
|
||||
fun click(load: LoadClickCallback) {
|
||||
loadResult(load.response.url, load.response.apiName, load.action)
|
||||
}
|
||||
|
||||
// only save the key if it is from UI, as we don't want internal functions changing the setting
|
||||
fun loadAndCancel(
|
||||
preferredApiName: String?,
|
||||
forceReload: Boolean = true,
|
||||
fromUI: Boolean = false
|
||||
) =
|
||||
ioSafe {
|
||||
// Since plugins are loaded in stages this function can get called multiple times.
|
||||
// The issue with this is that the homepage may be fetched multiple times while the first request is loading
|
||||
val api = getApiFromNameNull(preferredApiName)
|
||||
if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) {
|
||||
return@launchSafe
|
||||
// api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true
|
||||
val currentPage = page.value
|
||||
|
||||
// if we don't need to reload and we have a valid homepage or currently loading the same thing then return
|
||||
val currentLoading = isCurrentlyLoadingName
|
||||
if (!forceReload && (currentPage is Resource.Success && currentPage.value.isNotEmpty() || (currentLoading != null && currentLoading == preferredApiName))) {
|
||||
return@ioSafe
|
||||
}
|
||||
|
||||
if (preferredApiName == noneApi.name) {
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
|
||||
// just set to random
|
||||
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
|
||||
loadAndCancel(noneApi)
|
||||
} else if (preferredApiName == randomApi.name) {
|
||||
// randomize the api, if none exist like if not loaded or not installed
|
||||
// then use nothing
|
||||
val validAPIs = context?.filterProviderByPreferredMedia()
|
||||
if (validAPIs.isNullOrEmpty()) {
|
||||
// Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded
|
||||
loadAndCancel(noneApi)
|
||||
} else {
|
||||
val apiRandom = validAPIs.random()
|
||||
loadAndCancel(apiRandom)
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
|
||||
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
|
||||
}
|
||||
// If the plugin isn't loaded yet. (Does not set the key)
|
||||
} else if (api == null) {
|
||||
loadAndCancel(noneApi)
|
||||
// API is not found aka not loaded or removed, post the loading
|
||||
// progress if waiting for plugins, otherwise nothing
|
||||
if(PluginManager.loadedLocalPlugins) {
|
||||
loadAndCancel(noneApi)
|
||||
} else {
|
||||
_page.postValue(Resource.Loading())
|
||||
}
|
||||
} else {
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, api.name)
|
||||
// if the api is found, then set it to it and save key
|
||||
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name)
|
||||
loadAndCancel(api)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.content.res.Configuration
|
|||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -14,6 +13,7 @@ import android.view.animation.AlphaAnimation
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.lagradost.cloudstream3.APIHolder
|
||||
|
@ -22,6 +22,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
|
@ -37,7 +38,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
|
|||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
import kotlinx.android.synthetic.main.fragment_library.*
|
||||
import kotlin.math.abs
|
||||
|
||||
const val LIBRARY_FOLDER = "library_folder"
|
||||
|
@ -73,14 +73,25 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
private val libraryViewModel: LibraryViewModel by activityViewModels()
|
||||
|
||||
var binding: FragmentLibraryBinding? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_library, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
|
||||
//return inflater.inflate(R.layout.fragment_library, container, false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
viewpager?.currentItem?.let { currentItem ->
|
||||
binding?.viewpager?.currentItem?.let { currentItem ->
|
||||
outState.putInt(VIEWPAGER_ITEM_KEY, currentItem)
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
|
@ -88,9 +99,9 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(search_status_bar_padding)
|
||||
fixPaddingStatusbar(binding?.searchStatusBarPadding)
|
||||
|
||||
sort_fab?.setOnClickListener {
|
||||
binding?.sortFab?.setOnClickListener {
|
||||
val methods = libraryViewModel.sortingMethods.map {
|
||||
txt(it.stringRes).asString(view.context)
|
||||
}
|
||||
|
@ -106,7 +117,7 @@ class LibraryFragment : Fragment() {
|
|||
})
|
||||
}
|
||||
|
||||
main_search?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
libraryViewModel.sort(ListSorting.Query, query)
|
||||
return true
|
||||
|
@ -129,7 +140,7 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
libraryViewModel.reloadPages(false)
|
||||
|
||||
list_selector?.setOnClickListener {
|
||||
binding?.listSelector?.setOnClickListener {
|
||||
val items = libraryViewModel.availableApiNames
|
||||
val currentItem = libraryViewModel.currentApiName.value
|
||||
|
||||
|
@ -152,12 +163,14 @@ class LibraryFragment : Fragment() {
|
|||
syncId: SyncIdName,
|
||||
apiName: String? = null,
|
||||
) {
|
||||
val availableProviders = allProviders.filter {
|
||||
it.supportedSyncNames.contains(syncId)
|
||||
}.map { it.name } +
|
||||
// Add the api if it exists
|
||||
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList())
|
||||
|
||||
val availableProviders = synchronized(allProviders) {
|
||||
allProviders.filter {
|
||||
it.supportedSyncNames.contains(syncId)
|
||||
}.map { it.name } +
|
||||
// Add the api if it exists
|
||||
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
|
||||
?: emptyList())
|
||||
}
|
||||
val baseOptions = listOf(
|
||||
LibraryOpenerType.Default,
|
||||
LibraryOpenerType.None,
|
||||
|
@ -209,20 +222,22 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
provider_selector?.setOnClickListener {
|
||||
binding?.providerSelector?.setOnClickListener {
|
||||
val syncName = libraryViewModel.currentSyncApi?.syncIdName ?: return@setOnClickListener
|
||||
activity?.showPluginSelectionDialog(syncName.name, syncName)
|
||||
}
|
||||
|
||||
viewpager?.setPageTransformer(LibraryScrollTransformer())
|
||||
viewpager?.adapter =
|
||||
viewpager.adapter ?: ViewpagerAdapter(mutableListOf(), { isScrollingDown: Boolean ->
|
||||
if (isScrollingDown) {
|
||||
sort_fab?.shrink()
|
||||
} else {
|
||||
sort_fab?.extend()
|
||||
}
|
||||
}) callback@{ searchClickCallback ->
|
||||
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
|
||||
binding?.viewpager?.adapter =
|
||||
binding?.viewpager?.adapter ?: ViewpagerAdapter(
|
||||
mutableListOf(),
|
||||
{ isScrollingDown: Boolean ->
|
||||
if (isScrollingDown) {
|
||||
binding?.sortFab?.shrink()
|
||||
} else {
|
||||
binding?.sortFab?.extend()
|
||||
}
|
||||
}) callback@{ searchClickCallback ->
|
||||
// To prevent future accidents
|
||||
debugAssert({
|
||||
searchClickCallback.card !is SyncAPI.LibraryItem
|
||||
|
@ -267,6 +282,7 @@ class LibraryFragment : Fragment() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
LibraryOpenerType.None -> {}
|
||||
LibraryOpenerType.Provider ->
|
||||
savedSelection.providerData?.apiName?.let { apiName ->
|
||||
|
@ -275,8 +291,10 @@ class LibraryFragment : Fragment() {
|
|||
apiName,
|
||||
)
|
||||
}
|
||||
|
||||
LibraryOpenerType.Browser ->
|
||||
openBrowser(searchClickCallback.card.url)
|
||||
|
||||
LibraryOpenerType.Search -> {
|
||||
QuickSearchFragment.pushSearch(
|
||||
activity,
|
||||
|
@ -288,22 +306,28 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewpager?.offscreenPageLimit = 2
|
||||
viewpager?.reduceDragSensitivity()
|
||||
binding?.apply {
|
||||
viewpager.offscreenPageLimit = 2
|
||||
viewpager.reduceDragSensitivity()
|
||||
}
|
||||
|
||||
val startLoading = Runnable {
|
||||
gridview?.numColumns = context?.getSpanCount() ?: 3
|
||||
gridview?.adapter =
|
||||
context?.let { LoadingPosterAdapter(it, 6 * 3) }
|
||||
library_loading_overlay?.isVisible = true
|
||||
library_loading_shimmer?.startShimmer()
|
||||
empty_list_textview?.isVisible = false
|
||||
binding?.apply {
|
||||
gridview.numColumns = context?.getSpanCount() ?: 3
|
||||
gridview.adapter =
|
||||
context?.let { LoadingPosterAdapter(it, 6 * 3) }
|
||||
libraryLoadingOverlay.isVisible = true
|
||||
libraryLoadingShimmer.startShimmer()
|
||||
emptyListTextview.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
val stopLoading = Runnable {
|
||||
gridview?.adapter = null
|
||||
library_loading_overlay?.isVisible = false
|
||||
library_loading_shimmer?.stopShimmer()
|
||||
binding?.apply {
|
||||
gridview.adapter = null
|
||||
libraryLoadingOverlay.isVisible = false
|
||||
libraryLoadingShimmer.stopShimmer()
|
||||
}
|
||||
}
|
||||
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
|
@ -314,65 +338,75 @@ class LibraryFragment : Fragment() {
|
|||
handler.removeCallbacks(startLoading)
|
||||
val pages = resource.value
|
||||
val showNotice = pages.all { it.items.isEmpty() }
|
||||
empty_list_textview?.isVisible = showNotice
|
||||
if (showNotice) {
|
||||
if (libraryViewModel.availableApiNames.size > 1) {
|
||||
empty_list_textview?.setText(R.string.empty_library_logged_in_message)
|
||||
} else {
|
||||
empty_list_textview?.setText(R.string.empty_library_no_accounts_message)
|
||||
|
||||
|
||||
binding?.apply {
|
||||
emptyListTextview.isVisible = showNotice
|
||||
if (showNotice) {
|
||||
if (libraryViewModel.availableApiNames.size > 1) {
|
||||
emptyListTextview.setText(R.string.empty_library_logged_in_message)
|
||||
} else {
|
||||
emptyListTextview.setText(R.string.empty_library_no_accounts_message)
|
||||
}
|
||||
}
|
||||
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
|
||||
// Using notifyItemRangeChanged keeps the animations when sorting
|
||||
viewpager.adapter?.notifyItemRangeChanged(
|
||||
0,
|
||||
viewpager.adapter?.itemCount ?: 0
|
||||
)
|
||||
|
||||
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
|
||||
// Without this there would be a flashing effect:
|
||||
// loading -> show old viewpager -> black screen -> show new viewpager
|
||||
handler.postDelayed(stopLoading, 300)
|
||||
|
||||
savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos ->
|
||||
if (currentPos < 0) return@let
|
||||
viewpager.setCurrentItem(currentPos, false)
|
||||
// Using remove() sets the key to 0 instead of removing it
|
||||
savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1)
|
||||
}
|
||||
|
||||
// Since the animation to scroll multiple items is so much its better to just hide
|
||||
// the viewpager a bit while the fastest animation is running
|
||||
fun hideViewpager(distance: Int) {
|
||||
if (distance < 3) return
|
||||
|
||||
val hideAnimation = AlphaAnimation(1f, 0f).apply {
|
||||
duration = distance * 50L
|
||||
fillAfter = true
|
||||
}
|
||||
val showAnimation = AlphaAnimation(0f, 1f).apply {
|
||||
duration = distance * 50L
|
||||
startOffset = distance * 100L
|
||||
fillAfter = true
|
||||
}
|
||||
viewpager.startAnimation(hideAnimation)
|
||||
viewpager.startAnimation(showAnimation)
|
||||
}
|
||||
|
||||
TabLayoutMediator(
|
||||
libraryTabLayout,
|
||||
viewpager,
|
||||
) { tab, position ->
|
||||
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
|
||||
tab.view.setOnClickListener {
|
||||
val currentItem =
|
||||
binding?.viewpager?.currentItem ?: return@setOnClickListener
|
||||
val distance = abs(position - currentItem)
|
||||
hideViewpager(distance)
|
||||
}
|
||||
}.attach()
|
||||
}
|
||||
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.pages = pages
|
||||
// Using notifyItemRangeChanged keeps the animations when sorting
|
||||
viewpager.adapter?.notifyItemRangeChanged(0, viewpager.adapter?.itemCount ?: 0)
|
||||
|
||||
// Only stop loading after 300ms to hide the fade effect the viewpager produces when updating
|
||||
// Without this there would be a flashing effect:
|
||||
// loading -> show old viewpager -> black screen -> show new viewpager
|
||||
handler.postDelayed(stopLoading, 300)
|
||||
|
||||
savedInstanceState?.getInt(VIEWPAGER_ITEM_KEY)?.let { currentPos ->
|
||||
if (currentPos < 0) return@let
|
||||
viewpager?.setCurrentItem(currentPos, false)
|
||||
// Using remove() sets the key to 0 instead of removing it
|
||||
savedInstanceState.putInt(VIEWPAGER_ITEM_KEY, -1)
|
||||
}
|
||||
|
||||
// Since the animation to scroll multiple items is so much its better to just hide
|
||||
// the viewpager a bit while the fastest animation is running
|
||||
fun hideViewpager(distance: Int) {
|
||||
if (distance < 3) return
|
||||
|
||||
val hideAnimation = AlphaAnimation(1f, 0f).apply {
|
||||
duration = distance * 50L
|
||||
fillAfter = true
|
||||
}
|
||||
val showAnimation = AlphaAnimation(0f, 1f).apply {
|
||||
duration = distance * 50L
|
||||
startOffset = distance * 100L
|
||||
fillAfter = true
|
||||
}
|
||||
viewpager?.startAnimation(hideAnimation)
|
||||
viewpager?.startAnimation(showAnimation)
|
||||
}
|
||||
|
||||
TabLayoutMediator(
|
||||
library_tab_layout,
|
||||
viewpager,
|
||||
) { tab, position ->
|
||||
tab.text = pages.getOrNull(position)?.title?.asStringNull(context)
|
||||
tab.view.setOnClickListener {
|
||||
val currentItem = viewpager?.currentItem ?: return@setOnClickListener
|
||||
val distance = abs(position - currentItem)
|
||||
hideViewpager(distance)
|
||||
}
|
||||
}.attach()
|
||||
}
|
||||
|
||||
is Resource.Loading -> {
|
||||
// Only start loading after 200ms to prevent loading cached lists
|
||||
handler.postDelayed(startLoading, 200)
|
||||
}
|
||||
|
||||
is Resource.Failure -> {
|
||||
stopLoading.run()
|
||||
// No user indication it failed :(
|
||||
|
@ -383,7 +417,7 @@ class LibraryFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
(viewpager.adapter as? ViewpagerAdapter)?.rebind()
|
||||
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind()
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,16 +2,14 @@ package com.lagradost.cloudstream3.ui.library
|
|||
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.doOnAttach
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.OnFlingListener
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
import kotlinx.android.synthetic.main.library_viewpager_page.view.*
|
||||
|
||||
class ViewpagerAdapter(
|
||||
var pages: List<SyncAPI.Page>,
|
||||
|
@ -20,8 +18,7 @@ class ViewpagerAdapter(
|
|||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return PageViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.library_viewpager_page, parent, false)
|
||||
LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -34,6 +31,7 @@ class ViewpagerAdapter(
|
|||
}
|
||||
|
||||
private val unbound = mutableSetOf<Int>()
|
||||
|
||||
/**
|
||||
* Used to mark all pages for re-binding and forces all items to be refreshed
|
||||
* Without this the pages will still use the same adapters
|
||||
|
@ -43,44 +41,46 @@ class ViewpagerAdapter(
|
|||
this.notifyItemRangeChanged(0, pages.size)
|
||||
}
|
||||
|
||||
inner class PageViewHolder(private val itemViewTest: View) :
|
||||
RecyclerView.ViewHolder(itemViewTest) {
|
||||
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(page: SyncAPI.Page, rebind: Boolean) {
|
||||
itemView.page_recyclerview?.spanCount =
|
||||
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
|
||||
|
||||
if (itemViewTest.page_recyclerview?.adapter == null || rebind) {
|
||||
// Only add the items after it has been attached since the items rely on ItemWidth
|
||||
// Which is only determined after the recyclerview is attached.
|
||||
// If this fails then item height becomes 0 when there is only one item
|
||||
itemViewTest.page_recyclerview?.doOnAttach {
|
||||
itemViewTest.page_recyclerview?.adapter = PageAdapter(
|
||||
page.items.toMutableList(),
|
||||
itemViewTest.page_recyclerview,
|
||||
clickCallback
|
||||
)
|
||||
binding.pageRecyclerview.apply {
|
||||
spanCount =
|
||||
this@PageViewHolder.itemView.context.getSpanCount() ?: 3
|
||||
if (adapter == null || rebind) {
|
||||
// Only add the items after it has been attached since the items rely on ItemWidth
|
||||
// Which is only determined after the recyclerview is attached.
|
||||
// If this fails then item height becomes 0 when there is only one item
|
||||
doOnAttach {
|
||||
adapter = PageAdapter(
|
||||
page.items.toMutableList(),
|
||||
this,
|
||||
clickCallback
|
||||
)
|
||||
}
|
||||
} else {
|
||||
(adapter as? PageAdapter)?.updateList(page.items)
|
||||
scrollToPosition(0)
|
||||
}
|
||||
} else {
|
||||
(itemViewTest.page_recyclerview?.adapter as? PageAdapter)?.updateList(page.items)
|
||||
itemViewTest.page_recyclerview?.scrollToPosition(0)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
itemViewTest.page_recyclerview.setOnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
|
||||
val diff = scrollY - oldScrollY
|
||||
if (diff == 0) return@setOnScrollChangeListener
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
val diff = scrollY - oldScrollY
|
||||
if (diff == 0) return@setOnScrollChangeListener
|
||||
|
||||
scrollCallback.invoke(diff > 0)
|
||||
}
|
||||
} else {
|
||||
itemViewTest.page_recyclerview.onFlingListener = object : OnFlingListener() {
|
||||
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
|
||||
scrollCallback.invoke(velocityY > 0)
|
||||
return false
|
||||
scrollCallback.invoke(diff > 0)
|
||||
}
|
||||
} else {
|
||||
onFlingListener = object : OnFlingListener() {
|
||||
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
|
||||
scrollCallback.invoke(velocityY > 0)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.annotation.StringRes
|
||||
|
@ -41,8 +44,6 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip
|
|||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import kotlinx.android.synthetic.main.fragment_player.*
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.*
|
||||
|
||||
enum class PlayerResize(@StringRes val nameRes: Int) {
|
||||
Fit(R.string.resize_fit),
|
||||
|
@ -71,9 +72,15 @@ abstract class AbstractPlayerFragment(
|
|||
var isBuffering = true
|
||||
protected open var hasPipModeSupport = true
|
||||
|
||||
var playerPausePlayHolderHolder : FrameLayout? = null
|
||||
var playerPausePlay : ImageView? = null
|
||||
var playerBuffering : ProgressBar? = null
|
||||
var playerView : PlayerView? = null
|
||||
var piphide : FrameLayout? = null
|
||||
var subtitleHolder : FrameLayout? = null
|
||||
|
||||
@LayoutRes
|
||||
protected var layout: Int = R.layout.fragment_player
|
||||
protected open var layout: Int = R.layout.fragment_player
|
||||
|
||||
open fun nextEpisode() {
|
||||
throw NotImplementedError()
|
||||
|
@ -132,15 +139,15 @@ abstract class AbstractPlayerFragment(
|
|||
|
||||
isBuffering = CSPlayerLoading.IsBuffering == isPlaying
|
||||
if (isBuffering) {
|
||||
player_pause_play_holder_holder?.isVisible = false
|
||||
player_buffering?.isVisible = true
|
||||
playerPausePlayHolderHolder?.isVisible = false
|
||||
playerBuffering?.isVisible = true
|
||||
} else {
|
||||
player_pause_play_holder_holder?.isVisible = true
|
||||
player_buffering?.isVisible = false
|
||||
playerPausePlayHolderHolder?.isVisible = true
|
||||
playerBuffering?.isVisible = false
|
||||
|
||||
if (wasPlaying != isPlaying) {
|
||||
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
|
||||
val drawable = player_pause_play?.drawable
|
||||
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.play_to_pause else R.drawable.pause_to_play)
|
||||
val drawable = playerPausePlay?.drawable
|
||||
|
||||
var startedAnimation = false
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
|
||||
|
@ -162,10 +169,10 @@ abstract class AbstractPlayerFragment(
|
|||
|
||||
// somehow the phone is wacked
|
||||
if (!startedAnimation) {
|
||||
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
|
||||
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
|
||||
}
|
||||
} else {
|
||||
player_pause_play?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
|
||||
playerPausePlay?.setImageResource(if (isPlayingRightNow) R.drawable.netflix_pause else R.drawable.netflix_play)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,14 +251,12 @@ abstract class AbstractPlayerFragment(
|
|||
fun showToast(message: String, gotoNext: Boolean = false) {
|
||||
if (gotoNext && hasNextMirror()) {
|
||||
showToast(
|
||||
activity,
|
||||
message,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
nextMirror()
|
||||
} else {
|
||||
showToast(
|
||||
activity,
|
||||
context?.getString(R.string.no_links_found_toast) + "\n" + message,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
@ -327,9 +332,9 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
|
||||
// Necessary for multiple combined videos
|
||||
player_view?.setShowMultiWindowTimeBar(true)
|
||||
player_view?.player = player
|
||||
player_view?.performClick()
|
||||
playerView?.setShowMultiWindowTimeBar(true)
|
||||
playerView?.player = player
|
||||
playerView?.performClick()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,9 +391,9 @@ abstract class AbstractPlayerFragment(
|
|||
)
|
||||
|
||||
if (player is CS3IPlayer) {
|
||||
subView = player_view?.findViewById(R.id.exo_subtitles)
|
||||
subView = playerView?.findViewById(R.id.exo_subtitles)
|
||||
subStyle = SubtitlesFragment.getCurrentSavedStyle()
|
||||
player.initSubtitles(subView, subtitle_holder, subStyle)
|
||||
player.initSubtitles(subView, subtitleHolder, subStyle)
|
||||
|
||||
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged
|
||||
|
||||
|
@ -457,10 +462,10 @@ abstract class AbstractPlayerFragment(
|
|||
PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
PlayerResize.Zoom -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
|
||||
}
|
||||
player_view?.resizeMode = type
|
||||
playerView?.resizeMode = type
|
||||
|
||||
if (showToast)
|
||||
showToast(activity, resize.nameRes, Toast.LENGTH_SHORT)
|
||||
showToast(resize.nameRes, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -481,6 +486,13 @@ abstract class AbstractPlayerFragment(
|
|||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(layout, container, false)
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
playerPausePlayHolderHolder = root.findViewById(R.id.player_pause_play_holder_holder)
|
||||
playerPausePlay = root.findViewById(R.id.player_pause_play)
|
||||
playerBuffering = root.findViewById(R.id.player_buffering)
|
||||
playerView = root.findViewById(R.id.player_view)
|
||||
piphide = root.findViewById(R.id.piphide)
|
||||
subtitleHolder = root.findViewById(R.id.subtitle_holder)
|
||||
return root
|
||||
}
|
||||
}
|
|
@ -63,7 +63,15 @@ import javax.net.ssl.SSLSession
|
|||
const val TAG = "CS3ExoPlayer"
|
||||
const val PREFERRED_AUDIO_LANGUAGE_KEY = "preferred_audio_language"
|
||||
|
||||
/** Cache */
|
||||
/** toleranceBeforeUs – The maximum time that the actual position seeked to may precede the
|
||||
* requested seek position, in microseconds. Must be non-negative. */
|
||||
const val toleranceBeforeUs = 300_000L
|
||||
|
||||
/**
|
||||
* toleranceAfterUs – The maximum time that the actual position seeked to may exceed the requested
|
||||
* seek position, in microseconds. Must be non-negative.
|
||||
*/
|
||||
const val toleranceAfterUs = 300_000L
|
||||
|
||||
class CS3IPlayer : IPlayer {
|
||||
private var isPlaying = false
|
||||
|
@ -721,7 +729,7 @@ class CS3IPlayer : IPlayer {
|
|||
)
|
||||
)
|
||||
// Allows any seeking to be +- 0.3s to allow for faster seeking
|
||||
.setSeekParameters(SeekParameters(300_000, 300_000))
|
||||
.setSeekParameters(SeekParameters(toleranceBeforeUs, toleranceAfterUs))
|
||||
.setLoadControl(
|
||||
DefaultLoadControl.Builder()
|
||||
.setTargetBufferBytes(
|
||||
|
@ -788,7 +796,7 @@ class CS3IPlayer : IPlayer {
|
|||
private fun getCurrentTimestamp(writePosition: Long? = null): EpisodeSkip.SkipStamp? {
|
||||
val position = writePosition ?: this@CS3IPlayer.getPosition() ?: return null
|
||||
for (lastTimeStamp in lastTimeStamps) {
|
||||
if (lastTimeStamp.startMs <= position && position < lastTimeStamp.endMs) {
|
||||
if (lastTimeStamp.startMs <= position && (position + (toleranceBeforeUs / 1000L) + 1) < lastTimeStamp.endMs) {
|
||||
return lastTimeStamp
|
||||
}
|
||||
}
|
||||
|
@ -796,11 +804,12 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
fun updatedTime(writePosition: Long? = null) {
|
||||
getCurrentTimestamp(writePosition)?.let { timestamp ->
|
||||
val position = writePosition ?: exoPlayer?.currentPosition
|
||||
|
||||
getCurrentTimestamp(position)?.let { timestamp ->
|
||||
onTimestampInvoked?.invoke(timestamp)
|
||||
}
|
||||
|
||||
val position = writePosition ?: exoPlayer?.currentPosition
|
||||
val duration = exoPlayer?.contentDuration
|
||||
if (duration != null && position != null) {
|
||||
playerPositionChanged?.invoke(Pair(position, duration))
|
||||
|
@ -1086,9 +1095,9 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
override fun onRenderedFirstFrame() {
|
||||
updatedTime()
|
||||
super.onRenderedFirstFrame()
|
||||
onRenderFirst()
|
||||
updatedTime()
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
|
@ -1116,42 +1125,43 @@ class CS3IPlayer : IPlayer {
|
|||
}
|
||||
|
||||
fun onRenderFirst() {
|
||||
if (!hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||
Log.i(TAG, "Rendered first frame")
|
||||
val invalid = exoPlayer?.duration?.let { duration ->
|
||||
// Only errors short playback when not playing downloaded files
|
||||
duration < 20_000L && currentDownloadedFile == null
|
||||
// Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period
|
||||
// If you can get the total time that'd be better, but this is already niche.
|
||||
&& exoPlayer?.currentTimeline?.periodCount == 1
|
||||
&& exoPlayer?.isCurrentMediaItemLive != true
|
||||
} ?: false
|
||||
if (hasUsedFirstRender) { // this insures that we only call this once per player load
|
||||
return
|
||||
}
|
||||
Log.i(TAG, "Rendered first frame")
|
||||
hasUsedFirstRender = true
|
||||
val invalid = exoPlayer?.duration?.let { duration ->
|
||||
// Only errors short playback when not playing downloaded files
|
||||
duration < 20_000L && currentDownloadedFile == null
|
||||
// Concatenated sources (non 1 periodCount) bypasses the invalid check as exoPlayer.duration gives only the current period
|
||||
// If you can get the total time that'd be better, but this is already niche.
|
||||
&& exoPlayer?.currentTimeline?.periodCount == 1
|
||||
&& exoPlayer?.isCurrentMediaItemLive != true
|
||||
} ?: false
|
||||
|
||||
if (invalid) {
|
||||
releasePlayer(saveTime = false)
|
||||
playerError?.invoke(InvalidFileException("Too short playback"))
|
||||
return
|
||||
}
|
||||
if (invalid) {
|
||||
releasePlayer(saveTime = false)
|
||||
playerError?.invoke(InvalidFileException("Too short playback"))
|
||||
return
|
||||
}
|
||||
|
||||
setPreferredSubtitles(currentSubtitles)
|
||||
hasUsedFirstRender = true
|
||||
val format = exoPlayer?.videoFormat
|
||||
val width = format?.width
|
||||
val height = format?.height
|
||||
if (height != null && width != null) {
|
||||
playerDimensionsLoaded?.invoke(Pair(width, height))
|
||||
updatedTime()
|
||||
exoPlayer?.apply {
|
||||
requestedListeningPercentages?.forEach { percentage ->
|
||||
createMessage { _, _ ->
|
||||
updatedTime()
|
||||
}
|
||||
.setLooper(Looper.getMainLooper())
|
||||
.setPosition( /* positionMs= */contentDuration * percentage / 100)
|
||||
// .setPayload(customPayloadData)
|
||||
.setDeleteAfterDelivery(false)
|
||||
.send()
|
||||
setPreferredSubtitles(currentSubtitles)
|
||||
val format = exoPlayer?.videoFormat
|
||||
val width = format?.width
|
||||
val height = format?.height
|
||||
if (height != null && width != null) {
|
||||
playerDimensionsLoaded?.invoke(Pair(width, height))
|
||||
updatedTime()
|
||||
exoPlayer?.apply {
|
||||
requestedListeningPercentages?.forEach { percentage ->
|
||||
createMessage { _, _ ->
|
||||
updatedTime()
|
||||
}
|
||||
.setLooper(Looper.getMainLooper())
|
||||
.setPosition(contentDuration * percentage / 100)
|
||||
// .setPayload(customPayloadData)
|
||||
.setDeleteAfterDelivery(false)
|
||||
.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -25,6 +25,11 @@ import com.hippo.unifile.UniFile
|
|||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.databinding.DialogOnlineSubtitlesBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerCustomLayoutBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectSourceAndSubsBinding
|
||||
import com.lagradost.cloudstream3.databinding.PlayerSelectTracksBinding
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.subtitles.AbstractSubtitleEntities
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.subtitleProviders
|
||||
|
@ -33,8 +38,6 @@ import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForced
|
|||
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriority
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.SourcePriorityDialog
|
||||
import com.lagradost.cloudstream3.ui.result.*
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
|
||||
|
@ -49,18 +52,8 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.*
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.apply_btt
|
||||
import kotlinx.android.synthetic.main.dialog_online_subtitles.cancel_btt
|
||||
import kotlinx.android.synthetic.main.fragment_player.*
|
||||
import kotlinx.android.synthetic.main.player_custom_layout.*
|
||||
import kotlinx.android.synthetic.main.player_select_source_and_subs.*
|
||||
import kotlinx.android.synthetic.main.player_select_source_and_subs.subtitles_click_settings
|
||||
import kotlinx.android.synthetic.main.player_select_tracks.*
|
||||
import kotlinx.coroutines.Job
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.math.abs
|
||||
|
||||
class GeneratorPlayer : FullScreenPlayer() {
|
||||
|
@ -98,12 +91,14 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
private var preferredAutoSelectSubtitles: String? = null // null means do nothing, "" means none
|
||||
|
||||
private var binding: FragmentPlayerBinding? = null
|
||||
|
||||
private fun startLoading() {
|
||||
player.release()
|
||||
currentSelectedSubtitles = null
|
||||
isActive = false
|
||||
overlay_loading_skip_button?.isVisible = false
|
||||
player_loading_overlay?.isVisible = true
|
||||
binding?.overlayLoadingSkipButton?.isVisible = false
|
||||
binding?.playerLoadingOverlay?.isVisible = true
|
||||
}
|
||||
|
||||
private fun setSubtitles(sub: SubtitleData?): Boolean {
|
||||
|
@ -118,7 +113,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
override fun onTracksInfoChanged() {
|
||||
val tracks = player.getVideoTracks()
|
||||
player_tracks_btt?.isVisible =
|
||||
playerBinding?.playerTracksBtt?.isVisible =
|
||||
tracks.allVideoTracks.size > 1 || tracks.allAudioTracks.size > 1
|
||||
// Only set the preferred language if it is available.
|
||||
// Otherwise it may give some users audio track init failed!
|
||||
|
@ -158,12 +153,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
if (link == null) return
|
||||
|
||||
// manage UI
|
||||
player_loading_overlay?.isVisible = false
|
||||
binding?.playerLoadingOverlay?.isVisible = false
|
||||
uiReset()
|
||||
currentSelectedLink = link
|
||||
currentMeta = viewModel.getMeta()
|
||||
nextMeta = viewModel.getNextMeta()
|
||||
setEpisodes(viewModel.getAllMeta() ?: emptyList())
|
||||
// setEpisodes(viewModel.getAllMeta() ?: emptyList())
|
||||
isActive = true
|
||||
setPlayerDimen(null)
|
||||
setTitle()
|
||||
|
@ -209,7 +204,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
closestQuality(linkData?.quality)
|
||||
)
|
||||
val sourcePriority =
|
||||
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.name)
|
||||
QualityDataHelper.getSourcePriority(qualityProfile, linkData?.source)
|
||||
|
||||
// negative because we want to sort highest quality first
|
||||
return qualityPriority + sourcePriority
|
||||
|
@ -257,7 +252,9 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val isSingleProvider = subsProviders.size == 1
|
||||
|
||||
val dialog = Dialog(context, R.style.AlertDialogCustomBlack)
|
||||
dialog.setContentView(R.layout.dialog_online_subtitles)
|
||||
val binding =
|
||||
DialogOnlineSubtitlesBinding.inflate(LayoutInflater.from(context), null, false)
|
||||
dialog.setContentView(binding.root)
|
||||
|
||||
var currentSubtitles: List<AbstractSubtitleEntities.SubtitleEntity> = emptyList()
|
||||
var currentSubtitle: AbstractSubtitleEntities.SubtitleEntity? = null
|
||||
|
@ -295,6 +292,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
imageViewEnd.setImageDrawable(drawableEnd)
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view = convertView ?: LayoutInflater.from(context).inflate(layout, null)
|
||||
|
||||
|
@ -318,16 +316,16 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
dialog.show()
|
||||
dialog.cancel_btt.setOnClickListener {
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
dialog.dismissSafe()
|
||||
}
|
||||
|
||||
dialog.subtitle_adapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
dialog.subtitle_adapter.adapter = arrayAdapter
|
||||
binding.subtitleAdapter.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
binding.subtitleAdapter.adapter = arrayAdapter
|
||||
val adapter =
|
||||
dialog.subtitle_adapter.adapter as? ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>
|
||||
binding.subtitleAdapter.adapter as? ArrayAdapter<AbstractSubtitleEntities.SubtitleEntity>
|
||||
|
||||
dialog.subtitle_adapter.setOnItemClickListener { _, _, position, _ ->
|
||||
binding.subtitleAdapter.setOnItemClickListener { _, _, position, _ ->
|
||||
currentSubtitle = currentSubtitles.getOrNull(position) ?: return@setOnItemClickListener
|
||||
}
|
||||
|
||||
|
@ -343,16 +341,16 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val currentTempMeta = getMetaData()
|
||||
// bruh idk why it is not correct
|
||||
val color = ColorStateList.valueOf(context.colorFromAttribute(R.attr.colorAccent))
|
||||
dialog.search_loading_bar.progressTintList = color
|
||||
dialog.search_loading_bar.indeterminateTintList = color
|
||||
binding.searchLoadingBar.progressTintList = color
|
||||
binding.searchLoadingBar.indeterminateTintList = color
|
||||
|
||||
observeNullable(viewModel.currentSubtitleYear) {
|
||||
// When year is changed search again
|
||||
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
|
||||
dialog.year_btt.text = it?.toString() ?: txt(R.string.none).asString(context)
|
||||
binding.subtitlesSearch.setQuery(binding.subtitlesSearch.query, true)
|
||||
binding.yearBtt.text = it?.toString() ?: txt(R.string.none).asString(context)
|
||||
}
|
||||
|
||||
dialog.year_btt?.setOnClickListener {
|
||||
binding.yearBtt.setOnClickListener {
|
||||
val none = txt(R.string.none).asString(context)
|
||||
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
|
||||
val earliestYear = 1900
|
||||
|
@ -380,10 +378,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
)
|
||||
}
|
||||
|
||||
dialog.subtitles_search.setOnQueryTextListener(object :
|
||||
binding.subtitlesSearch.setOnQueryTextListener(object :
|
||||
androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
dialog.search_loading_bar?.show()
|
||||
binding.searchLoadingBar.show()
|
||||
ioSafe {
|
||||
val search =
|
||||
AbstractSubtitleEntities.SubtitleSearch(
|
||||
|
@ -415,7 +413,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
// ugly ik
|
||||
activity?.runOnUiThread {
|
||||
setSubtitlesList(items)
|
||||
dialog.search_loading_bar?.hide()
|
||||
binding.searchLoadingBar.hide()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -427,7 +425,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
})
|
||||
|
||||
dialog.search_filter.setOnClickListener { view ->
|
||||
binding.searchFilter.setOnClickListener { view ->
|
||||
val lang639_1 = languages.map { it.ISO_639_1 }
|
||||
activity?.showDialog(languages.map { it.languageName },
|
||||
lang639_1.indexOf(currentLanguageTwoLetters),
|
||||
|
@ -436,11 +434,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
true,
|
||||
{ }) { index ->
|
||||
currentLanguageTwoLetters = lang639_1[index]
|
||||
dialog.subtitles_search.setQuery(dialog.subtitles_search.query, true)
|
||||
binding.subtitlesSearch.setQuery(binding.subtitlesSearch.query, true)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.apply_btt.setOnClickListener {
|
||||
binding.applyBtt.setOnClickListener {
|
||||
currentSubtitle?.let { currentSubtitle ->
|
||||
providers.firstOrNull { it.idPrefix == currentSubtitle.idPrefix }?.let { api ->
|
||||
ioSafe {
|
||||
|
@ -466,7 +464,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
dialog.show()
|
||||
dialog.subtitles_search.setQuery(currentTempMeta.name, true)
|
||||
binding.subtitlesSearch.setQuery(currentTempMeta.name, true)
|
||||
//TODO: Set year text from currently loaded movie on Player
|
||||
//dialog.subtitles_search_year?.setText(currentTempMeta.year)
|
||||
}
|
||||
|
@ -509,7 +507,6 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
selectSourceDialog?.dismissSafe()
|
||||
|
||||
showToast(
|
||||
activity,
|
||||
String.format(ctx.getString(R.string.player_loaded_subtitles), subtitleData.name),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
@ -558,13 +555,15 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
val currentSubtitles = sortSubs(currentSubs)
|
||||
|
||||
val sourceDialog = Dialog(ctx, R.style.AlertDialogCustomBlack)
|
||||
sourceDialog.setContentView(R.layout.player_select_source_and_subs)
|
||||
val binding =
|
||||
PlayerSelectSourceAndSubsBinding.inflate(LayoutInflater.from(ctx), null, false)
|
||||
sourceDialog.setContentView(binding.root)
|
||||
|
||||
selectSourceDialog = sourceDialog
|
||||
|
||||
sourceDialog.show()
|
||||
val providerList = sourceDialog.sort_providers
|
||||
val subtitleList = sourceDialog.sort_subtitles
|
||||
val providerList = binding.sortProviders
|
||||
val subtitleList = binding.sortSubtitles
|
||||
|
||||
val loadFromFileFooter: TextView =
|
||||
layoutInflater.inflate(R.layout.sort_bottom_footer_add_choice, null) as TextView
|
||||
|
@ -672,12 +671,12 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
sourceDialog.cancel_btt?.setOnClickListener {
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
sourceDialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
fun setProfileName(profile: Int) {
|
||||
sourceDialog.source_settings_btt.setText(
|
||||
binding.sourceSettingsBtt.setText(
|
||||
QualityDataHelper.getProfileName(
|
||||
profile
|
||||
)
|
||||
|
@ -685,7 +684,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
setProfileName(currentQualityProfile)
|
||||
|
||||
sourceDialog.profiles_click_settings.setOnClickListener {
|
||||
binding.profilesClickSettings.setOnClickListener {
|
||||
val activity = activity ?: return@setOnClickListener
|
||||
QualityProfileDialog(
|
||||
activity,
|
||||
|
@ -699,7 +698,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}.show()
|
||||
}
|
||||
|
||||
sourceDialog.subtitles_encoding_format?.apply {
|
||||
binding.subtitlesEncodingFormat.apply {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
|
||||
|
@ -712,7 +711,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
text = prefNames[if (index == -1) 0 else index]
|
||||
}
|
||||
|
||||
sourceDialog.subtitles_click_settings?.setOnClickListener {
|
||||
binding.subtitlesClickSettings.setOnClickListener {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
val prefNames = ctx.resources.getStringArray(R.array.subtitles_encoding_list)
|
||||
|
@ -741,7 +740,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
|
||||
sourceDialog.apply_btt?.setOnClickListener {
|
||||
binding.applyBtt.setOnClickListener {
|
||||
var init = false
|
||||
if (sourceIndex != startSource) {
|
||||
init = true
|
||||
|
@ -781,18 +780,19 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
it.height?.times(-1)
|
||||
}
|
||||
val currentAudioTracks = tracks.allAudioTracks
|
||||
|
||||
val binding: PlayerSelectTracksBinding =
|
||||
PlayerSelectTracksBinding.inflate(LayoutInflater.from(ctx), null, false)
|
||||
val trackDialog = Dialog(ctx, R.style.AlertDialogCustomBlack)
|
||||
trackDialog.setContentView(R.layout.player_select_tracks)
|
||||
trackDialog.setContentView(binding.root)
|
||||
trackDialog.show()
|
||||
|
||||
// selectTracksDialog = tracksDialog
|
||||
|
||||
val videosList = trackDialog.video_tracks_list
|
||||
val audioList = trackDialog.auto_tracks_list
|
||||
val videosList = binding.videoTracksList
|
||||
val audioList = binding.autoTracksList
|
||||
|
||||
trackDialog.video_tracks_holder.isVisible = currentVideoTracks.size > 1
|
||||
trackDialog.audio_tracks_holder.isVisible = currentAudioTracks.size > 1
|
||||
binding.videoTracksHolder.isVisible = currentVideoTracks.size > 1
|
||||
binding.audioTracksHolder.isVisible = currentAudioTracks.size > 1
|
||||
|
||||
fun dismiss() {
|
||||
if (isPlaying) {
|
||||
|
@ -857,11 +857,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
audioList.setItemChecked(which, true)
|
||||
}
|
||||
|
||||
trackDialog.cancel_btt?.setOnClickListener {
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
trackDialog.dismissSafe(activity)
|
||||
}
|
||||
|
||||
trackDialog.apply_btt?.setOnClickListener {
|
||||
binding.applyBtt.setOnClickListener {
|
||||
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
|
||||
player.setPreferredAudioTrack(
|
||||
currentTrack?.language, currentTrack?.id
|
||||
|
@ -889,7 +889,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
private fun noLinksFound() {
|
||||
showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
|
||||
|
@ -1030,8 +1030,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
if (meta.tvType.isAnimeOp()) isOpVisible = percentage < SKIP_OP_VIDEO_PERCENTAGE
|
||||
}
|
||||
}
|
||||
player_skip_op?.isVisible = isOpVisible
|
||||
player_skip_episode?.isVisible = !isOpVisible && viewModel.hasNextEpisode() == true
|
||||
|
||||
playerBinding?.playerSkipOp?.isVisible = isOpVisible
|
||||
playerBinding?.playerSkipEpisode?.isVisible =
|
||||
!isOpVisible && viewModel.hasNextEpisode() == true
|
||||
|
||||
if (percentage >= PRELOAD_NEXT_EPISODE_PERCENTAGE) {
|
||||
viewModel.preLoadNextLinks()
|
||||
|
@ -1168,7 +1170,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
//Hide title, if set in setting
|
||||
if (limitTitle < 0) {
|
||||
player_video_title?.visibility = View.GONE
|
||||
playerBinding?.playerVideoTitle?.visibility = View.GONE
|
||||
} else {
|
||||
//Truncate video title if it exceeds limit
|
||||
val differenceInLength = playerVideoTitle.length - limitTitle
|
||||
|
@ -1179,8 +1181,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
val isFiller: Boolean? = (currentMeta as? ResultEpisode)?.isFiller
|
||||
|
||||
player_episode_filler_holder?.isVisible = isFiller ?: false
|
||||
player_video_title?.text = playerVideoTitle
|
||||
playerBinding?.playerEpisodeFillerHolder?.isVisible = isFiller ?: false
|
||||
playerBinding?.playerVideoTitle?.text = playerVideoTitle
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
|
@ -1201,8 +1203,10 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
3 -> "$source - $extra"
|
||||
else -> ""
|
||||
}
|
||||
player_video_title_rez?.text = title
|
||||
player_video_title_rez?.isVisible = title.isNotBlank()
|
||||
playerBinding?.playerVideoTitleRez?.apply {
|
||||
text = title
|
||||
isVisible = title.isNotBlank()
|
||||
}
|
||||
}
|
||||
|
||||
override fun playerDimensionsLoaded(widthHeight: Pair<Int, Int>) {
|
||||
|
@ -1230,7 +1234,14 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
unwrapBundle(savedInstanceState)
|
||||
unwrapBundle(arguments)
|
||||
|
||||
return super.onCreateView(inflater, container, savedInstanceState)
|
||||
val root = super.onCreateView(inflater, container, savedInstanceState) ?: return null
|
||||
binding = FragmentPlayerBinding.bind(root)
|
||||
return root
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
var timestampShowState = false
|
||||
|
@ -1243,7 +1254,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
skipIndex++
|
||||
println("displayTimeStamp = $show")
|
||||
timestampShowState = show
|
||||
skip_chapter_button?.apply {
|
||||
playerBinding?.skipChapterButton?.apply {
|
||||
val showWidth = 170.toPx
|
||||
val noShowWidth = 10.toPx
|
||||
//if((show && width == showWidth) || (!show && width == noShowWidth)) {
|
||||
|
@ -1263,7 +1274,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
from, to
|
||||
).apply {
|
||||
addListener(onEnd = {
|
||||
if (!show) skip_chapter_button?.isVisible = false
|
||||
if (!show) playerBinding?.skipChapterButton?.isVisible = false
|
||||
})
|
||||
addUpdateListener { valueAnimator ->
|
||||
val value = valueAnimator.animatedValue as Int
|
||||
|
@ -1283,10 +1294,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
|
||||
override fun onTimestamp(timestamp: EpisodeSkip.SkipStamp?) {
|
||||
if (timestamp != null) {
|
||||
skip_chapter_button.setText(timestamp.uiText)
|
||||
println("timestamp: $timestamp")
|
||||
playerBinding?.skipChapterButton?.setText(timestamp.uiText)
|
||||
displayTimeStamp(true)
|
||||
val currentIndex = skipIndex
|
||||
skip_chapter_button?.handler?.postDelayed({
|
||||
playerBinding?.skipChapterButton?.handler?.postDelayed({
|
||||
if (skipIndex == currentIndex)
|
||||
displayTimeStamp(false)
|
||||
}, 6000)
|
||||
|
@ -1329,11 +1341,11 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
viewModel.loadLinks()
|
||||
}
|
||||
|
||||
overlay_loading_skip_button?.setOnClickListener {
|
||||
binding?.overlayLoadingSkipButton?.setOnClickListener {
|
||||
startPlayer()
|
||||
}
|
||||
|
||||
player_loading_go_back?.setOnClickListener {
|
||||
binding?.playerLoadingGoBack?.setOnClickListener {
|
||||
player.release()
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
|
@ -1357,7 +1369,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
is Resource.Failure -> {
|
||||
showToast(activity, it.errorString, Toast.LENGTH_LONG)
|
||||
showToast(it.errorString, Toast.LENGTH_LONG)
|
||||
startPlayer()
|
||||
}
|
||||
}
|
||||
|
@ -1366,8 +1378,8 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
observe(viewModel.currentLinks) {
|
||||
currentLinks = it
|
||||
val turnVisible = it.isNotEmpty()
|
||||
val wasGone = overlay_loading_skip_button?.isGone == true
|
||||
overlay_loading_skip_button?.isVisible = turnVisible
|
||||
val wasGone = binding?.overlayLoadingSkipButton?.isGone == true
|
||||
binding?.overlayLoadingSkipButton?.isVisible = turnVisible
|
||||
|
||||
normalSafeApiCall {
|
||||
if (currentLinks.any { link ->
|
||||
|
@ -1380,7 +1392,7 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
|
||||
if (turnVisible && wasGone) {
|
||||
overlay_loading_skip_button?.requestFocus()
|
||||
binding?.overlayLoadingSkipButton?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1410,4 +1422,4 @@ class GeneratorPlayer : FullScreenPlayer() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
package com.lagradost.cloudstream3.ui.player.source_priority
|
||||
|
||||
import android.app.Dialog
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StyleRes
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.PlayerQualityProfileDialogBinding
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfileName
|
||||
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getProfiles
|
||||
import com.lagradost.cloudstream3.ui.result.txt
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import kotlinx.android.synthetic.main.player_quality_profile_dialog.*
|
||||
|
||||
class QualityProfileDialog(
|
||||
val activity: FragmentActivity,
|
||||
|
@ -24,83 +20,86 @@ class QualityProfileDialog(
|
|||
private val profileSelectionCallback: (QualityDataHelper.QualityProfile) -> Unit
|
||||
) : Dialog(activity, themeRes) {
|
||||
override fun show() {
|
||||
setContentView(R.layout.player_quality_profile_dialog)
|
||||
val profilesRecyclerView: RecyclerView = profiles_recyclerview
|
||||
|
||||
val binding = PlayerQualityProfileDialogBinding.inflate(this.layoutInflater, null, false)
|
||||
|
||||
setContentView(binding.root)//R.layout.player_quality_profile_dialog)
|
||||
/*val profilesRecyclerView: RecyclerView = profiles_recyclerview
|
||||
val useBtt: View = use_btt
|
||||
val editBtt: View = edit_btt
|
||||
val cancelBtt: View = cancel_btt
|
||||
val defaultBtt: View = set_default_btt
|
||||
val currentProfileText: TextView = currently_selected_profile_text
|
||||
val selectedItemActionsHolder: View = selected_item_holder
|
||||
|
||||
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
|
||||
return (profilesRecyclerView.adapter as? ProfilesAdapter)?.getCurrentProfile()
|
||||
}
|
||||
|
||||
fun refreshProfiles() {
|
||||
currentProfileText.text = getProfileName(usedProfile).asString(context)
|
||||
(profilesRecyclerView.adapter as? ProfilesAdapter)?.updateList(getProfiles())
|
||||
}
|
||||
|
||||
profilesRecyclerView.adapter = ProfilesAdapter(
|
||||
mutableListOf(),
|
||||
usedProfile,
|
||||
) { oldIndex: Int?, newIndex: Int ->
|
||||
profilesRecyclerView.adapter?.notifyItemChanged(newIndex)
|
||||
selectedItemActionsHolder.alpha = 1f
|
||||
if (oldIndex != null) {
|
||||
profilesRecyclerView.adapter?.notifyItemChanged(oldIndex)
|
||||
val selectedItemActionsHolder: View = selected_item_holder*/
|
||||
binding.apply {
|
||||
fun getCurrentProfile(): QualityDataHelper.QualityProfile? {
|
||||
return (profilesRecyclerview.adapter as? ProfilesAdapter)?.getCurrentProfile()
|
||||
}
|
||||
}
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
editBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let { profile ->
|
||||
SourcePriorityDialog(context, themeRes, links, profile) {
|
||||
refreshProfiles()
|
||||
}.show()
|
||||
fun refreshProfiles() {
|
||||
currentlySelectedProfileText.text = getProfileName(usedProfile).asString(context)
|
||||
(profilesRecyclerview.adapter as? ProfilesAdapter)?.updateList(getProfiles())
|
||||
}
|
||||
|
||||
profilesRecyclerview.adapter = ProfilesAdapter(
|
||||
mutableListOf(),
|
||||
usedProfile,
|
||||
) { oldIndex: Int?, newIndex: Int ->
|
||||
profilesRecyclerview.adapter?.notifyItemChanged(newIndex)
|
||||
selectedItemHolder.alpha = 1f
|
||||
if (oldIndex != null) {
|
||||
profilesRecyclerview.adapter?.notifyItemChanged(oldIndex)
|
||||
}
|
||||
}
|
||||
|
||||
refreshProfiles()
|
||||
|
||||
editBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let { profile ->
|
||||
SourcePriorityDialog(context, themeRes, links, profile) {
|
||||
refreshProfiles()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
defaultBtt.setOnClickListener {
|
||||
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
|
||||
val choices = QualityDataHelper.QualityProfileType.values()
|
||||
.filter { it != QualityDataHelper.QualityProfileType.None }
|
||||
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
|
||||
setDefaultBtt.setOnClickListener {
|
||||
val currentProfile = getCurrentProfile() ?: return@setOnClickListener
|
||||
val choices = QualityDataHelper.QualityProfileType.values()
|
||||
.filter { it != QualityDataHelper.QualityProfileType.None }
|
||||
val choiceNames = choices.map { txt(it.stringRes).asString(context) }
|
||||
|
||||
activity.showBottomDialog(
|
||||
choiceNames,
|
||||
choices.indexOf(currentProfile.type),
|
||||
txt(R.string.set_default).asString(context),
|
||||
false,
|
||||
{},
|
||||
{ index ->
|
||||
val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog
|
||||
// Remove previous picks
|
||||
if (pickedChoice.unique) {
|
||||
getProfiles().filter { it.type == pickedChoice }.forEach {
|
||||
QualityDataHelper.setQualityProfileType(it.id, null)
|
||||
activity.showBottomDialog(
|
||||
choiceNames,
|
||||
choices.indexOf(currentProfile.type),
|
||||
txt(R.string.set_default).asString(context),
|
||||
false,
|
||||
{},
|
||||
{ index ->
|
||||
val pickedChoice = choices.getOrNull(index) ?: return@showBottomDialog
|
||||
// Remove previous picks
|
||||
if (pickedChoice.unique) {
|
||||
getProfiles().filter { it.type == pickedChoice }.forEach {
|
||||
QualityDataHelper.setQualityProfileType(it.id, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice)
|
||||
refreshProfiles()
|
||||
})
|
||||
}
|
||||
QualityDataHelper.setQualityProfileType(currentProfile.id, pickedChoice)
|
||||
refreshProfiles()
|
||||
})
|
||||
}
|
||||
|
||||
cancelBtt.setOnClickListener {
|
||||
this.dismissSafe()
|
||||
}
|
||||
cancelBtt.setOnClickListener {
|
||||
this@QualityProfileDialog.dismissSafe()
|
||||
}
|
||||
|
||||
useBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let {
|
||||
profileSelectionCallback.invoke(it)
|
||||
this.dismissSafe()
|
||||
useBtt.setOnClickListener {
|
||||
getCurrentProfile()?.let {
|
||||
profileSelectionCallback.invoke(it)
|
||||
this@QualityProfileDialog.dismissSafe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.show()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,18 +3,16 @@ package com.lagradost.cloudstream3.ui.result
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.lagradost.cloudstream3.ActorData
|
||||
import com.lagradost.cloudstream3.ActorRole
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.CastItemBinding
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.cast_item.view.*
|
||||
|
||||
class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
data class ActorMetaData(
|
||||
var isInverted: Boolean,
|
||||
val actor: ActorData,
|
||||
|
@ -24,7 +22,7 @@ class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return CardViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(R.layout.cast_item, parent, false),
|
||||
CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -68,15 +66,10 @@ class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||
|
||||
private class CardViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
val binding: CastItemBinding,
|
||||
private val focusCallback : (View?) -> Unit = {}
|
||||
) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
private val actorImage: ImageView = itemView.actor_image
|
||||
private val actorName: TextView = itemView.actor_name
|
||||
private val actorExtra: TextView = itemView.actor_extra
|
||||
private val voiceActorImage: ImageView = itemView.voice_actor_image
|
||||
private val voiceActorImageHolder: View = itemView.voice_actor_image_holder
|
||||
private val voiceActorName: TextView = itemView.voice_actor_name
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(actor: ActorData, isInverted: Boolean, position: Int, callback: (Int) -> Unit) {
|
||||
val (mainImg, vaImage) = if (!isInverted || actor.voiceActor?.image.isNullOrBlank()) {
|
||||
|
@ -85,43 +78,53 @@ class ActorAdaptor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
|||
Pair(actor.voiceActor?.image, actor.actor.image)
|
||||
}
|
||||
|
||||
itemView.setOnFocusChangeListener { v, hasFocus ->
|
||||
if(hasFocus) {
|
||||
focusCallback(v)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
callback(position)
|
||||
}
|
||||
|
||||
actorImage.setImage(mainImg)
|
||||
binding.apply {
|
||||
actorImage.setImage(mainImg)
|
||||
|
||||
actorName.text = actor.actor.name
|
||||
actor.role?.let {
|
||||
actorExtra.context?.getString(
|
||||
when (it) {
|
||||
ActorRole.Main -> {
|
||||
R.string.actor_main
|
||||
}
|
||||
ActorRole.Supporting -> {
|
||||
R.string.actor_supporting
|
||||
}
|
||||
ActorRole.Background -> {
|
||||
R.string.actor_background
|
||||
actorName.text = actor.actor.name
|
||||
actor.role?.let {
|
||||
actorExtra.context?.getString(
|
||||
when (it) {
|
||||
ActorRole.Main -> {
|
||||
R.string.actor_main
|
||||
}
|
||||
|
||||
ActorRole.Supporting -> {
|
||||
R.string.actor_supporting
|
||||
}
|
||||
|
||||
ActorRole.Background -> {
|
||||
R.string.actor_background
|
||||
}
|
||||
}
|
||||
)?.let { text ->
|
||||
actorExtra.isVisible = true
|
||||
actorExtra.text = text
|
||||
}
|
||||
)?.let { text ->
|
||||
} ?: actor.roleString?.let {
|
||||
actorExtra.isVisible = true
|
||||
actorExtra.text = text
|
||||
actorExtra.text = it
|
||||
} ?: run {
|
||||
actorExtra.isVisible = false
|
||||
}
|
||||
} ?: actor.roleString?.let {
|
||||
actorExtra.isVisible = true
|
||||
actorExtra.text = it
|
||||
} ?: run {
|
||||
actorExtra.isVisible = false
|
||||
}
|
||||
|
||||
if (actor.voiceActor == null) {
|
||||
voiceActorImageHolder.isVisible = false
|
||||
voiceActorName.isVisible = false
|
||||
} else {
|
||||
voiceActorName.text = actor.voiceActor.name
|
||||
voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage)
|
||||
if (actor.voiceActor == null) {
|
||||
voiceActorImageHolder.isVisible = false
|
||||
voiceActorName.isVisible = false
|
||||
} else {
|
||||
voiceActorName.text = actor.voiceActor.name
|
||||
voiceActorImageHolder.isVisible = voiceActorImage.setImage(vaImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,34 +3,24 @@ package com.lagradost.cloudstream3.ui.result
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.ContentLoadingProgressBar
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding
|
||||
import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonViewHolder
|
||||
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
|
||||
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import kotlinx.android.synthetic.main.result_episode.view.*
|
||||
import kotlinx.android.synthetic.main.result_episode.view.episode_text
|
||||
import kotlinx.android.synthetic.main.result_episode_large.view.*
|
||||
import kotlinx.android.synthetic.main.result_episode_large.view.episode_filler
|
||||
import kotlinx.android.synthetic.main.result_episode_large.view.episode_progress
|
||||
import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_download
|
||||
import kotlinx.android.synthetic.main.result_episode_large.view.result_episode_progress_downloaded
|
||||
import java.util.*
|
||||
|
||||
const val ACTION_PLAY_EPISODE_IN_PLAYER = 1
|
||||
|
@ -59,7 +49,8 @@ const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16
|
|||
const val ACTION_PLAY_EPISODE_IN_MPV = 17
|
||||
|
||||
const val ACTION_MARK_AS_WATCHED = 18
|
||||
|
||||
const val TV_EP_SIZE_LARGE = 400
|
||||
const val TV_EP_SIZE_SMALL = 300
|
||||
data class EpisodeClickEvent(val action: Int, val data: ResultEpisode)
|
||||
|
||||
class EpisodeAdapter(
|
||||
|
@ -88,49 +79,10 @@ class EpisodeAdapter(
|
|||
|
||||
var cardList: MutableList<ResultEpisode> = mutableListOf()
|
||||
|
||||
private val mBoundViewHolders: HashSet<DownloadButtonViewHolder> = HashSet()
|
||||
private fun getAllBoundViewHolders(): Set<DownloadButtonViewHolder?>? {
|
||||
return Collections.unmodifiableSet(mBoundViewHolders)
|
||||
}
|
||||
|
||||
fun killAdapter() {
|
||||
getAllBoundViewHolders()?.forEach { view ->
|
||||
view?.downloadButton?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
||||
if (holder.itemView.hasFocus()) {
|
||||
holder.itemView.clearFocus()
|
||||
}
|
||||
//(holder.itemView as? FrameLayout?)?.descendantFocusability =
|
||||
// ViewGroup.FOCUS_BLOCK_DESCENDANTS
|
||||
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.downloadButton.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
holder.downloadButton.dispose()
|
||||
mBoundViewHolders.remove(holder)
|
||||
//(holder.itemView as? FrameLayout?)?.descendantFocusability =
|
||||
// ViewGroup.FOCUS_BLOCK_DESCENDANTS
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
||||
if (holder is DownloadButtonViewHolder) {
|
||||
//println("onViewAttachedToWindow = ${holder.absoluteAdapterPosition}")
|
||||
//holder.itemView.post {
|
||||
// if (holder.itemView.isAttachedToWindow)
|
||||
// (holder.itemView as? FrameLayout?)?.descendantFocusability =
|
||||
// ViewGroup.FOCUS_AFTER_DESCENDANTS
|
||||
//}
|
||||
|
||||
holder.reattachDownloadButton()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateList(newList: List<ResultEpisode>) {
|
||||
|
@ -144,27 +96,62 @@ class EpisodeAdapter(
|
|||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
var layout = R.layout.result_episode_both
|
||||
private fun getItem(position: Int): ResultEpisode {
|
||||
return cardList[position]
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = getItem(position)
|
||||
return if (item.poster.isNullOrBlank()) 0 else 1
|
||||
}
|
||||
|
||||
|
||||
// private val layout = R.layout.result_episode_both
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
/*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2)
|
||||
R.layout.result_episode_large
|
||||
else R.layout.result_episode*/
|
||||
|
||||
return EpisodeCardViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(layout, parent, false),
|
||||
hasDownloadSupport,
|
||||
clickCallback,
|
||||
downloadClickCallback
|
||||
)
|
||||
return when (viewType) {
|
||||
0 -> {
|
||||
EpisodeCardViewHolderSmall(
|
||||
ResultEpisodeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
hasDownloadSupport,
|
||||
clickCallback,
|
||||
downloadClickCallback
|
||||
)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
EpisodeCardViewHolderLarge(
|
||||
ResultEpisodeLargeBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
hasDownloadSupport,
|
||||
clickCallback,
|
||||
downloadClickCallback
|
||||
)
|
||||
}
|
||||
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder) {
|
||||
is EpisodeCardViewHolder -> {
|
||||
holder.bind(cardList[position])
|
||||
mBoundViewHolders.add(holder)
|
||||
is EpisodeCardViewHolderLarge -> {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
is EpisodeCardViewHolderSmall -> {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -173,94 +160,108 @@ class EpisodeAdapter(
|
|||
return cardList.size
|
||||
}
|
||||
|
||||
class EpisodeCardViewHolder
|
||||
class EpisodeCardViewHolderLarge
|
||||
constructor(
|
||||
itemView: View,
|
||||
val binding: ResultEpisodeLargeBinding,
|
||||
private val hasDownloadSupport: Boolean,
|
||||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
|
||||
) : RecyclerView.ViewHolder(itemView), DownloadButtonViewHolder {
|
||||
override var downloadButton = EasyDownloadButton()
|
||||
|
||||
var episodeDownloadBar: ContentLoadingProgressBar? = null
|
||||
var episodeDownloadImage: ImageView? = null
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
var localCard: ResultEpisode? = null
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(card: ResultEpisode) {
|
||||
localCard = card
|
||||
|
||||
val setWidth =
|
||||
if (isTvSettings()) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|
||||
binding.episodeLinHolder.layoutParams.width = setWidth
|
||||
binding.episodeHolderLarge.layoutParams.width = setWidth
|
||||
binding.episodeHolder.layoutParams.width = setWidth
|
||||
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
|
||||
val (parentView, otherView) = if (card.poster == null) {
|
||||
itemView.episode_holder to itemView.episode_holder_large
|
||||
} else {
|
||||
itemView.episode_holder_large to itemView.episode_holder
|
||||
}
|
||||
parentView.isVisible = true
|
||||
otherView.isVisible = false
|
||||
binding.apply {
|
||||
downloadButton.isVisible = hasDownloadSupport
|
||||
downloadButton.setDefaultClickListener(
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name,
|
||||
card.poster,
|
||||
card.episode,
|
||||
card.season,
|
||||
card.id,
|
||||
card.parentId,
|
||||
card.rating,
|
||||
card.description,
|
||||
System.currentTimeMillis(),
|
||||
), null
|
||||
) {
|
||||
when (it.action) {
|
||||
DOWNLOAD_ACTION_DOWNLOAD -> {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
|
||||
}
|
||||
|
||||
val episodeText: TextView = parentView.episode_text
|
||||
val episodeFiller: MaterialButton? = parentView.episode_filler
|
||||
val episodeRating: TextView? = parentView.episode_rating
|
||||
val episodeDescript: TextView? = parentView.episode_descript
|
||||
val episodeProgress: ContentLoadingProgressBar? = parentView.episode_progress
|
||||
val episodePoster: ImageView? = parentView.episode_poster
|
||||
DOWNLOAD_ACTION_LONG_CLICK -> {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card))
|
||||
}
|
||||
|
||||
episodeDownloadBar =
|
||||
parentView.result_episode_progress_downloaded
|
||||
episodeDownloadImage = parentView.result_episode_download
|
||||
|
||||
val name =
|
||||
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
|
||||
episodeFiller?.isVisible = card.isFiller == true
|
||||
episodeText.text =
|
||||
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
|
||||
episodeText.isSelected = true // is needed for text repeating
|
||||
|
||||
if (card.videoWatchState == VideoWatchState.Watched) {
|
||||
// This cannot be done in getDisplayPosition() as when you have not watched something
|
||||
// the duration and position is 0
|
||||
episodeProgress?.max = 1
|
||||
episodeProgress?.progress = 1
|
||||
episodeProgress?.isVisible = true
|
||||
} else {
|
||||
val displayPos = card.getDisplayPosition()
|
||||
episodeProgress?.max = (card.duration / 1000).toInt()
|
||||
episodeProgress?.progress = (displayPos / 1000).toInt()
|
||||
episodeProgress?.isVisible = displayPos > 0L
|
||||
}
|
||||
|
||||
episodePoster?.isVisible = episodePoster?.setImage(card.poster) == true
|
||||
|
||||
if (card.rating != null) {
|
||||
episodeRating?.text = episodeRating?.context?.getString(R.string.rated_format)
|
||||
?.format(card.rating.toFloat() / 10f)
|
||||
} else {
|
||||
episodeRating?.text = ""
|
||||
}
|
||||
|
||||
episodeRating?.isGone = episodeRating?.text.isNullOrBlank()
|
||||
|
||||
episodeDescript?.apply {
|
||||
text = card.description.html()
|
||||
isGone = text.isNullOrBlank()
|
||||
setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTrueTv) {
|
||||
episodePoster?.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
else -> {
|
||||
downloadClickCallback.invoke(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
episodePoster?.setOnLongClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card))
|
||||
return@setOnLongClickListener true
|
||||
val name =
|
||||
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
|
||||
episodeFiller.isVisible = card.isFiller == true
|
||||
episodeText.text =
|
||||
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
|
||||
episodeText.isSelected = true // is needed for text repeating
|
||||
|
||||
if (card.videoWatchState == VideoWatchState.Watched) {
|
||||
// This cannot be done in getDisplayPosition() as when you have not watched something
|
||||
// the duration and position is 0
|
||||
episodeProgress.max = 1
|
||||
episodeProgress.progress = 1
|
||||
episodeProgress.isVisible = true
|
||||
} else {
|
||||
val displayPos = card.getDisplayPosition()
|
||||
episodeProgress.max = (card.duration / 1000).toInt()
|
||||
episodeProgress.progress = (displayPos / 1000).toInt()
|
||||
episodeProgress.isVisible = displayPos > 0L
|
||||
}
|
||||
|
||||
episodePoster.isVisible = episodePoster.setImage(card.poster) == true
|
||||
|
||||
if (card.rating != null) {
|
||||
episodeRating.text = episodeRating.context?.getString(R.string.rated_format)
|
||||
?.format(card.rating.toFloat() / 10f)
|
||||
} else {
|
||||
episodeRating.text = ""
|
||||
}
|
||||
|
||||
episodeRating.isGone = episodeRating.text.isNullOrBlank()
|
||||
|
||||
episodeDescript.apply {
|
||||
text = card.description.html()
|
||||
isGone = text.isNullOrBlank()
|
||||
setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card))
|
||||
}
|
||||
}
|
||||
|
||||
if (!isTrueTv) {
|
||||
episodePoster.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
||||
episodePoster.setOnLongClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card))
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
@ -276,29 +277,30 @@ class EpisodeAdapter(
|
|||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
episodeDownloadImage?.isVisible = hasDownloadSupport
|
||||
episodeDownloadBar?.isVisible = hasDownloadSupport
|
||||
reattachDownloadButton()
|
||||
//binding.resultEpisodeDownload.isVisible = hasDownloadSupport
|
||||
//binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport
|
||||
}
|
||||
}
|
||||
|
||||
override fun reattachDownloadButton() {
|
||||
downloadButton.dispose()
|
||||
val card = localCard
|
||||
if (hasDownloadSupport && card != null) {
|
||||
if (episodeDownloadBar == null ||
|
||||
episodeDownloadImage == null
|
||||
) return
|
||||
val downloadInfo = VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
|
||||
itemView.context,
|
||||
card.id
|
||||
)
|
||||
class EpisodeCardViewHolderSmall
|
||||
constructor(
|
||||
val binding: ResultEpisodeBinding,
|
||||
private val hasDownloadSupport: Boolean,
|
||||
private val clickCallback: (EpisodeClickEvent) -> Unit,
|
||||
private val downloadClickCallback: (DownloadClickEvent) -> Unit,
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
@SuppressLint("SetTextI18n")
|
||||
fun bind(card: ResultEpisode) {
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
|
||||
downloadButton.setUpButton(
|
||||
downloadInfo?.fileLength,
|
||||
downloadInfo?.totalBytes,
|
||||
episodeDownloadBar ?: return,
|
||||
episodeDownloadImage ?: return,
|
||||
null,
|
||||
binding.episodeHolder.layoutParams.apply {
|
||||
width =
|
||||
if (isTvSettings()) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
binding.apply {
|
||||
downloadButton.isVisible = hasDownloadSupport
|
||||
downloadButton.setDefaultClickListener(
|
||||
VideoDownloadHelper.DownloadEpisodeCached(
|
||||
card.name,
|
||||
card.poster,
|
||||
|
@ -309,14 +311,60 @@ class EpisodeAdapter(
|
|||
card.rating,
|
||||
card.description,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
), null
|
||||
) {
|
||||
if (it.action == DOWNLOAD_ACTION_DOWNLOAD) {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
|
||||
} else {
|
||||
downloadClickCallback.invoke(it)
|
||||
when (it.action) {
|
||||
DOWNLOAD_ACTION_DOWNLOAD -> {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card))
|
||||
}
|
||||
|
||||
DOWNLOAD_ACTION_LONG_CLICK -> {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card))
|
||||
}
|
||||
|
||||
else -> {
|
||||
downloadClickCallback.invoke(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val name =
|
||||
if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}"
|
||||
episodeFiller.isVisible = card.isFiller == true
|
||||
episodeText.text =
|
||||
name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name
|
||||
episodeText.isSelected = true // is needed for text repeating
|
||||
|
||||
if (card.videoWatchState == VideoWatchState.Watched) {
|
||||
// This cannot be done in getDisplayPosition() as when you have not watched something
|
||||
// the duration and position is 0
|
||||
episodeProgress.max = 1
|
||||
episodeProgress.progress = 1
|
||||
episodeProgress.isVisible = true
|
||||
} else {
|
||||
val displayPos = card.getDisplayPosition()
|
||||
episodeProgress.max = (card.duration / 1000).toInt()
|
||||
episodeProgress.progress = (displayPos / 1000).toInt()
|
||||
episodeProgress.isVisible = displayPos > 0L
|
||||
}
|
||||
|
||||
itemView.setOnClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
|
||||
}
|
||||
|
||||
if (isTrueTv) {
|
||||
itemView.isFocusable = true
|
||||
itemView.isFocusableInTouchMode = true
|
||||
//itemView.touchscreenBlocksFocus = false
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card))
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
//binding.resultEpisodeDownload.isVisible = hasDownloadSupport
|
||||
//binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,33 +1,81 @@
|
|||
package com.lagradost.cloudstream3.ui.result
|
||||
|
||||
import android.animation.Animator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.mvvm.ResourceSome
|
||||
import com.lagradost.cloudstream3.mvvm.Some
|
||||
import com.lagradost.cloudstream3.databinding.FragmentResultTvBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup
|
||||
import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator
|
||||
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment.getStoredData
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
||||
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.html
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
|
||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
|
||||
import com.lagradost.cloudstream3.utils.UIHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import kotlinx.android.synthetic.main.fragment_result_tv.*
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
|
||||
class ResultFragmentTv : ResultFragment() {
|
||||
override val resultLayout = R.layout.fragment_result_tv
|
||||
class ResultFragmentTv : Fragment() {
|
||||
protected lateinit var viewModel: ResultViewModel2
|
||||
private var binding: FragmentResultTvBinding? = null
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
updateUIEvent -= ::updateUI
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
viewModel =
|
||||
ViewModelProvider(this)[ResultViewModel2::class.java]
|
||||
viewModel.EPISODE_RANGE_SIZE = 50
|
||||
updateUIEvent += ::updateUI
|
||||
|
||||
val localBinding = FragmentResultTvBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
}
|
||||
|
||||
private fun updateUI(id: Int?) {
|
||||
viewModel.reloadEpisodes()
|
||||
}
|
||||
|
||||
private var currentRecommendations: List<SearchResponse> = emptyList()
|
||||
|
||||
|
@ -36,12 +84,15 @@ class ResultFragmentTv : ResultFragment() {
|
|||
is EpisodeRange -> {
|
||||
viewModel.changeRange(data)
|
||||
}
|
||||
|
||||
is Int -> {
|
||||
viewModel.changeSeason(data)
|
||||
}
|
||||
|
||||
is DubStatus -> {
|
||||
viewModel.changeDubStatus(data)
|
||||
}
|
||||
|
||||
is String -> {
|
||||
setRecommendations(currentRecommendations, data)
|
||||
}
|
||||
|
@ -66,172 +117,640 @@ class ResultFragmentTv : ResultFragment() {
|
|||
private fun hasNoFocus(): Boolean {
|
||||
val focus = activity?.currentFocus
|
||||
if (focus == null || !focus.isVisible) return true
|
||||
return focus == this.result_root
|
||||
return focus == binding?.resultRoot
|
||||
}
|
||||
|
||||
override fun updateEpisodes(episodes: ResourceSome<List<ResultEpisode>>) {
|
||||
super.updateEpisodes(episodes)
|
||||
if (episodes is ResourceSome.Success && hasNoFocus()) {
|
||||
result_episodes?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateMovie(data: ResourceSome<Pair<UiText, ResultEpisode>>) {
|
||||
super.updateMovie(data)
|
||||
if (data is ResourceSome.Success && hasNoFocus()) {
|
||||
result_play_movie?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun setTrailers(trailers: List<ExtractorLink>?) {
|
||||
context?.updateHasTrailers()
|
||||
if (!LoadResponse.isTrailersEnabled) return
|
||||
|
||||
result_play_trailer?.isGone = trailers.isNullOrEmpty()
|
||||
result_play_trailer?.setOnClickListener {
|
||||
if (trailers.isNullOrEmpty()) return@setOnClickListener
|
||||
activity.navigate(
|
||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||
ExtractorLinkGenerator(
|
||||
trailers,
|
||||
emptyList()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
||||
private fun setRecommendations(rec: List<SearchResponse>?, validApiName: String?) {
|
||||
currentRecommendations = rec ?: emptyList()
|
||||
val isInvalid = rec.isNullOrEmpty()
|
||||
result_recommendations?.isGone = isInvalid
|
||||
result_recommendations_holder?.isGone = isInvalid
|
||||
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
|
||||
(result_recommendations?.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst }
|
||||
?: emptyList())
|
||||
binding?.apply {
|
||||
resultRecommendationsList.isGone = isInvalid
|
||||
resultRecommendationsHolder.isGone = isInvalid
|
||||
val matchAgainst = validApiName ?: rec?.firstOrNull()?.apiName
|
||||
(resultRecommendationsList.adapter as? SearchAdapter)?.updateList(rec?.filter { it.apiName == matchAgainst }
|
||||
?: emptyList())
|
||||
|
||||
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
|
||||
// very dirty selection
|
||||
result_recommendations_filter_selection?.isVisible = apiNames.size > 1
|
||||
result_recommendations_filter_selection?.update(apiNames.map { txt(it) to it })
|
||||
result_recommendations_filter_selection?.select(apiNames.indexOf(matchAgainst))
|
||||
} ?: run {
|
||||
result_recommendations_filter_selection?.isVisible = false
|
||||
rec?.map { it.apiName }?.distinct()?.let { apiNames ->
|
||||
// very dirty selection
|
||||
resultRecommendationsFilterSelection.isVisible = apiNames.size > 1
|
||||
resultRecommendationsFilterSelection.update(apiNames.map { txt(it) to it })
|
||||
resultRecommendationsFilterSelection.select(apiNames.indexOf(matchAgainst))
|
||||
} ?: run {
|
||||
resultRecommendationsFilterSelection.isVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var loadingDialog: Dialog? = null
|
||||
var popupDialog: Dialog? = null
|
||||
|
||||
private fun reloadViewModel(forceReload: Boolean) {
|
||||
if (!viewModel.hasLoaded() || forceReload) {
|
||||
val storedData = getStoredData() ?: return
|
||||
viewModel.load(
|
||||
activity,
|
||||
storedData.url,
|
||||
storedData.apiName,
|
||||
storedData.showFillers,
|
||||
storedData.dubStatus,
|
||||
storedData.start
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
activity?.let {
|
||||
it.window?.navigationBarColor =
|
||||
it.colorFromAttribute(R.attr.primaryBlackBackground)
|
||||
}
|
||||
afterPluginsLoadedEvent += ::reloadViewModel
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
afterPluginsLoadedEvent -= ::reloadViewModel
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
private fun View.fade(turnVisible: Boolean) {
|
||||
if (turnVisible) {
|
||||
isVisible = true
|
||||
}
|
||||
|
||||
this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply {
|
||||
duration = 200
|
||||
interpolator = DecelerateInterpolator()
|
||||
setListener(object : Animator.AnimatorListener {
|
||||
override fun onAnimationStart(animation: Animator) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
this@fade.isVisible = turnVisible
|
||||
}
|
||||
|
||||
override fun onAnimationCancel(animation: Animator) {
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animator) {
|
||||
}
|
||||
})
|
||||
}
|
||||
this.animate().translationX(if (turnVisible) 0f else if(isRtl()) -100.0f else 100f).apply {
|
||||
duration = 200
|
||||
interpolator = DecelerateInterpolator()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleEpisodes(show: Boolean) {
|
||||
binding?.apply {
|
||||
episodesShadow.fade(show)
|
||||
episodeHolderTv.fade(show)
|
||||
if(episodesShadow.isRtl()) {
|
||||
episodesShadow.scaleX = -1.0f
|
||||
episodesShadow.scaleY = -1.0f
|
||||
} else {
|
||||
episodesShadow.scaleX = 1.0f
|
||||
episodesShadow.scaleY = 1.0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
result_episodes?.layoutManager =
|
||||
//LinearListLayout(result_episodes ?: return, result_episodes?.context).apply {
|
||||
LinearListLayout(result_episodes?.context).apply {
|
||||
setHorizontal()
|
||||
// ===== setup =====
|
||||
val storedData = getStoredData() ?: return
|
||||
activity?.window?.decorView?.clearFocus()
|
||||
activity?.loadCache()
|
||||
hideKeyboard()
|
||||
if (storedData.restart || !viewModel.hasLoaded())
|
||||
viewModel.load(
|
||||
activity,
|
||||
storedData.url,
|
||||
storedData.apiName,
|
||||
storedData.showFillers,
|
||||
storedData.dubStatus,
|
||||
storedData.start
|
||||
)
|
||||
// ===== ===== =====
|
||||
|
||||
binding?.apply {
|
||||
//episodesShadow.rotationX = 180.0f//if(episodesShadow.isRtl()) 180.0f else 0.0f
|
||||
|
||||
val leftListener: View.OnFocusChangeListener =
|
||||
View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) return@OnFocusChangeListener
|
||||
toggleEpisodes(false)
|
||||
}
|
||||
|
||||
val rightListener: View.OnFocusChangeListener =
|
||||
View.OnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) return@OnFocusChangeListener
|
||||
toggleEpisodes(true)
|
||||
}
|
||||
|
||||
resultPlayMovie.onFocusChangeListener = leftListener
|
||||
resultPlaySeries.onFocusChangeListener = leftListener
|
||||
resultResumeSeries.onFocusChangeListener = leftListener
|
||||
resultPlayTrailer.onFocusChangeListener = leftListener
|
||||
resultEpisodesShow.onFocusChangeListener = rightListener
|
||||
resultDescription.onFocusChangeListener = leftListener
|
||||
resultBookmarkButton.onFocusChangeListener = leftListener
|
||||
resultEpisodesShow.setOnClickListener {
|
||||
// toggle, to make it more touch accessable just in case someone thinks that a
|
||||
// tv layout is better but is using a touch device
|
||||
toggleEpisodes(!episodeHolderTv.isVisible)
|
||||
}
|
||||
|
||||
// resultEpisodes.onFocusChangeListener = leftListener
|
||||
|
||||
redirectToPlay.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) return@setOnFocusChangeListener
|
||||
toggleEpisodes(false)
|
||||
|
||||
binding?.apply {
|
||||
val views = listOf(
|
||||
resultPlayMovie,
|
||||
resultPlaySeries,
|
||||
resultResumeSeries,
|
||||
resultPlayTrailer,
|
||||
resultBookmarkButton
|
||||
)
|
||||
for (requestView in views) {
|
||||
if (!requestView.isVisible) continue
|
||||
if (requestView.requestFocus()) break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parallax on background
|
||||
resultFinishLoading.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
backgroundPosterHolder.translationY = -scrollY.toFloat() * 0.8f
|
||||
})
|
||||
|
||||
redirectToEpisodes.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) return@setOnFocusChangeListener
|
||||
toggleEpisodes(true)
|
||||
binding?.apply {
|
||||
val views = listOf(
|
||||
resultSeasonSelection,
|
||||
resultRangeSelection,
|
||||
resultDubSelection,
|
||||
resultPlayTrailer,
|
||||
resultEpisodes
|
||||
)
|
||||
for (requestView in views) {
|
||||
if (!requestView.isShown) continue
|
||||
if (requestView.requestFocus()) break // View.FOCUS_RIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resultEpisodes.setLinearListLayout(isHorizontal = false)/*.layoutManager =
|
||||
LinearListLayout(resultEpisodes.context, resultEpisodes.isRtl()).apply {
|
||||
setVertical()
|
||||
}*/
|
||||
|
||||
resultReloadConnectionerror.setOnClickListener {
|
||||
viewModel.load(
|
||||
activity,
|
||||
storedData.url,
|
||||
storedData.apiName,
|
||||
storedData.showFillers,
|
||||
storedData.dubStatus,
|
||||
storedData.start
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
resultMetaSite.isFocusable = false
|
||||
|
||||
//resultReloadConnectionOpenInBrowser.setOnClickListener {view ->
|
||||
// view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true)
|
||||
//}
|
||||
|
||||
resultSeasonSelection.setAdapter()
|
||||
resultRangeSelection.setAdapter()
|
||||
resultDubSelection.setAdapter()
|
||||
resultRecommendationsFilterSelection.setAdapter()
|
||||
|
||||
resultCastItems.setOnFocusChangeListener { _, hasFocus ->
|
||||
// Always escape focus
|
||||
if (hasFocus) binding?.resultBookmarkButton?.requestFocus()
|
||||
}
|
||||
//resultBack.setOnClickListener {
|
||||
// activity?.popCurrentPage()
|
||||
//}
|
||||
|
||||
resultRecommendationsList.spanCount = 8
|
||||
resultRecommendationsList.adapter =
|
||||
SearchAdapter(
|
||||
ArrayList(),
|
||||
resultRecommendationsList,
|
||||
) { callback ->
|
||||
if(callback.action == SEARCH_ACTION_FOCUSED)
|
||||
toggleEpisodes(false)
|
||||
else
|
||||
SearchHelper.handleSearchClickCallback(callback)
|
||||
}
|
||||
|
||||
resultEpisodes.adapter =
|
||||
EpisodeAdapter(
|
||||
false,
|
||||
{ episodeClick ->
|
||||
viewModel.handleAction(episodeClick)
|
||||
},
|
||||
{ downloadClickEvent ->
|
||||
DownloadButtonSetup.handleDownloadClick(downloadClickEvent)
|
||||
}
|
||||
)
|
||||
|
||||
resultCastItems.layoutManager = object : LinearListLayout(view.context) {
|
||||
override fun onRequestChildFocus(
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State,
|
||||
child: View,
|
||||
focused: View?
|
||||
): Boolean {
|
||||
// Make the cast always focus the first visible item when focused
|
||||
// from somewhere else. Otherwise it jumps to the last item.
|
||||
return if (parent.focusedChild == null) {
|
||||
scrollToPosition(this.findFirstCompletelyVisibleItemPosition())
|
||||
true
|
||||
} else {
|
||||
super.onRequestChildFocus(parent, state, child, focused)
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
this.orientation = RecyclerView.HORIZONTAL
|
||||
}
|
||||
resultCastItems.adapter = ActorAdaptor {
|
||||
toggleEpisodes(false)
|
||||
}
|
||||
(result_episodes?.adapter as EpisodeAdapter?)?.apply {
|
||||
layout = R.layout.result_episode_both_tv
|
||||
}
|
||||
//result_episodes?.setMaxViewPoolSize(0, Int.MAX_VALUE)
|
||||
|
||||
result_season_selection.setAdapter()
|
||||
result_range_selection.setAdapter()
|
||||
result_dub_selection.setAdapter()
|
||||
result_recommendations_filter_selection.setAdapter()
|
||||
observeNullable(viewModel.resumeWatching) { resume ->
|
||||
binding?.apply {
|
||||
// show progress no matter if series or movie
|
||||
resume?.progress?.let { progress ->
|
||||
resultResumeSeriesProgressText.setText(progress.progressLeft)
|
||||
resultResumeSeriesProgress.apply {
|
||||
isVisible = true
|
||||
this.max = progress.maxProgress
|
||||
this.progress = progress.progress
|
||||
}
|
||||
resultResumeProgressHolder.isVisible = true
|
||||
} ?: run {
|
||||
resultResumeProgressHolder.isVisible = false
|
||||
}
|
||||
|
||||
observe(viewModel.selectPopup) { popup ->
|
||||
when (popup) {
|
||||
is Some.Success -> {
|
||||
popupDialog?.dismissSafe(activity)
|
||||
// if movie then hide both as movie button is
|
||||
// always visible on movies, this is done in movie observe
|
||||
|
||||
popupDialog = activity?.let { act ->
|
||||
val pop = popup.value
|
||||
val options = pop.getOptions(act)
|
||||
val title = pop.getTitle(act)
|
||||
if (resume?.isMovie == true) {
|
||||
resultPlaySeries.isVisible = false
|
||||
resultResumeSeries.isVisible = false
|
||||
return@observeNullable
|
||||
}
|
||||
|
||||
act.showBottomDialogInstant(
|
||||
options, title, {
|
||||
popupDialog = null
|
||||
pop.callback(null)
|
||||
}, {
|
||||
popupDialog = null
|
||||
pop.callback(it)
|
||||
}
|
||||
// if series then
|
||||
// > resultPlaySeries is visible when null
|
||||
// > resultResumeSeries is visible when not null
|
||||
if (resume == null) {
|
||||
resultPlaySeries.isVisible = true
|
||||
resultResumeSeries.isVisible = false
|
||||
return@observeNullable
|
||||
}
|
||||
|
||||
resultPlaySeries.isVisible = false
|
||||
resultResumeSeries.isVisible = true
|
||||
|
||||
if (hasNoFocus()) {
|
||||
resultResumeSeries.requestFocus()
|
||||
}
|
||||
|
||||
resultResumeSeries.text =
|
||||
if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull(
|
||||
null, // resume.result.name, we don't want episode title
|
||||
resume.result.episode,
|
||||
resume.result.season
|
||||
)
|
||||
|
||||
resultResumeSeries.setOnClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(
|
||||
storedData.playerAction, //?: ACTION_PLAY_EPISODE_IN_PLAYER,
|
||||
resume.result
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
resultResumeSeries.setOnLongClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(ACTION_SHOW_OPTIONS, resume.result)
|
||||
)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.trailers) { trailersLinks ->
|
||||
context?.updateHasTrailers()
|
||||
if (!LoadResponse.isTrailersEnabled) return@observe
|
||||
val trailers = trailersLinks.flatMap { it.mirros }
|
||||
binding?.resultPlayTrailer?.apply {
|
||||
isGone = trailers.isEmpty()
|
||||
setOnClickListener {
|
||||
if (trailers.isEmpty()) return@setOnClickListener
|
||||
activity.navigate(
|
||||
R.id.global_to_navigation_player, GeneratorPlayer.newInstance(
|
||||
ExtractorLinkGenerator(
|
||||
trailers,
|
||||
emptyList()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.watchStatus) { watchType ->
|
||||
binding?.resultBookmarkButton?.apply {
|
||||
setText(watchType.stringRes)
|
||||
setOnClickListener { view ->
|
||||
activity?.showBottomDialog(
|
||||
WatchType.values().map { view.context.getString(it.stringRes) }.toList(),
|
||||
watchType.ordinal,
|
||||
view.context.getString(R.string.action_add_to_bookmarks),
|
||||
showApply = false,
|
||||
{}) {
|
||||
viewModel.updateWatchStatus(WatchType.values()[it])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeNullable(viewModel.movie) { data ->
|
||||
binding?.apply {
|
||||
resultPlayMovie.isVisible = data is Resource.Success
|
||||
seriesHolder.isVisible = data == null
|
||||
resultEpisodesShow.isVisible = data == null
|
||||
|
||||
(data as? Resource.Success)?.value?.let { (text, ep) ->
|
||||
resultPlayMovie.setText(text)
|
||||
resultPlayMovie.setOnClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
|
||||
)
|
||||
}
|
||||
}
|
||||
is Some.None -> {
|
||||
popupDialog?.dismissSafe(activity)
|
||||
popupDialog = null
|
||||
resultPlayMovie.setOnLongClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(ACTION_SHOW_OPTIONS, ep)
|
||||
)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
if (hasNoFocus()) {
|
||||
resultPlayMovie.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observe(viewModel.loadedLinks) { load ->
|
||||
when (load) {
|
||||
is Some.Success -> {
|
||||
if (loadingDialog?.isShowing != true) {
|
||||
loadingDialog?.dismissSafe(activity)
|
||||
loadingDialog = null
|
||||
observeNullable(viewModel.selectPopup) { popup ->
|
||||
if (popup == null) {
|
||||
popupDialog?.dismissSafe(activity)
|
||||
popupDialog = null
|
||||
return@observeNullable
|
||||
}
|
||||
|
||||
popupDialog?.dismissSafe(activity)
|
||||
|
||||
popupDialog = activity?.let { act ->
|
||||
val options = popup.getOptions(act)
|
||||
val title = popup.getTitle(act)
|
||||
|
||||
act.showBottomDialogInstant(
|
||||
options, title, {
|
||||
popupDialog = null
|
||||
popup.callback(null)
|
||||
}, {
|
||||
popupDialog = null
|
||||
popup.callback(it)
|
||||
}
|
||||
loadingDialog = loadingDialog ?: context?.let { ctx ->
|
||||
val builder = BottomSheetDialog(ctx)
|
||||
builder.setContentView(R.layout.bottom_loading)
|
||||
builder.setOnDismissListener {
|
||||
loadingDialog = null
|
||||
viewModel.cancelLinks()
|
||||
}
|
||||
//builder.setOnCancelListener {
|
||||
// it?.dismiss()
|
||||
//}
|
||||
builder.setCanceledOnTouchOutside(true)
|
||||
builder.show()
|
||||
builder
|
||||
}
|
||||
}
|
||||
is Some.None -> {
|
||||
loadingDialog?.dismissSafe(activity)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
observeNullable(viewModel.loadedLinks) { load ->
|
||||
if (load == null) {
|
||||
loadingDialog?.dismissSafe(activity)
|
||||
loadingDialog = null
|
||||
return@observeNullable
|
||||
}
|
||||
if (loadingDialog?.isShowing != true) {
|
||||
loadingDialog?.dismissSafe(activity)
|
||||
loadingDialog = null
|
||||
}
|
||||
loadingDialog = loadingDialog ?: context?.let { ctx ->
|
||||
val builder = BottomSheetDialog(ctx)
|
||||
builder.setContentView(R.layout.bottom_loading)
|
||||
builder.setOnDismissListener {
|
||||
loadingDialog = null
|
||||
viewModel.cancelLinks()
|
||||
}
|
||||
//builder.setOnCancelListener {
|
||||
// it?.dismiss()
|
||||
//}
|
||||
builder.setCanceledOnTouchOutside(true)
|
||||
builder.show()
|
||||
builder
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
observe(viewModel.episodesCountText) { count ->
|
||||
result_episodes_text.setText(count)
|
||||
observeNullable(viewModel.episodesCountText) { count ->
|
||||
binding?.resultEpisodesText.setText(count)
|
||||
}
|
||||
|
||||
observe(viewModel.selectedRangeIndex) { selected ->
|
||||
result_range_selection.select(selected)
|
||||
binding?.resultRangeSelection.select(selected)
|
||||
}
|
||||
observe(viewModel.selectedSeasonIndex) { selected ->
|
||||
result_season_selection.select(selected)
|
||||
binding?.resultSeasonSelection.select(selected)
|
||||
}
|
||||
observe(viewModel.selectedDubStatusIndex) { selected ->
|
||||
result_dub_selection.select(selected)
|
||||
binding?.resultDubSelection.select(selected)
|
||||
}
|
||||
observe(viewModel.rangeSelections) {
|
||||
result_range_selection.update(it)
|
||||
binding?.resultRangeSelection.update(it)
|
||||
}
|
||||
observe(viewModel.dubSubSelections) {
|
||||
result_dub_selection.update(it)
|
||||
binding?.resultDubSelection.update(it)
|
||||
}
|
||||
observe(viewModel.seasonSelections) {
|
||||
result_season_selection.update(it)
|
||||
binding?.resultSeasonSelection.update(it)
|
||||
}
|
||||
|
||||
result_back?.setOnClickListener {
|
||||
activity?.popCurrentPage()
|
||||
observe(viewModel.recommendations) { recommendations ->
|
||||
setRecommendations(recommendations, null)
|
||||
}
|
||||
|
||||
result_recommendations?.spanCount = 8
|
||||
result_recommendations?.adapter =
|
||||
SearchAdapter(
|
||||
ArrayList(),
|
||||
result_recommendations,
|
||||
) { callback ->
|
||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
observe(viewModel.episodeSynopsis) { description ->
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
builder.setMessage(description.html())
|
||||
.setTitle(R.string.synopsis)
|
||||
.setOnDismissListener {
|
||||
viewModel.releaseEpisodeSynopsis()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
observeNullable(viewModel.episodes) { episodes ->
|
||||
binding?.apply {
|
||||
resultEpisodes.isVisible = episodes is Resource.Success
|
||||
// resultEpisodeLoading.isVisible = episodes is Resource.Loading
|
||||
if (episodes is Resource.Success) {
|
||||
val first = episodes.value.firstOrNull()
|
||||
if (first != null) {
|
||||
resultPlaySeries.text = context?.getNameFull(
|
||||
null, // resume.result.name, we don't want episode title
|
||||
first.episode,
|
||||
first.season
|
||||
)
|
||||
|
||||
resultPlaySeries.setOnClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(
|
||||
ACTION_PLAY_EPISODE_IN_PLAYER,
|
||||
first
|
||||
)
|
||||
)
|
||||
}
|
||||
resultPlaySeries.setOnLongClickListener {
|
||||
viewModel.handleAction(
|
||||
EpisodeClickEvent(ACTION_SHOW_OPTIONS, first)
|
||||
)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Okay so what is this fuckery?
|
||||
* Basically Android TV will crash if you request a new focus while
|
||||
* the adapter gets updated.
|
||||
*
|
||||
* This means that if you load thumbnails and request a next focus at the same time
|
||||
* the app will crash without any way to catch it!
|
||||
*
|
||||
* How to bypass this?
|
||||
* This code basically steals the focus for 500ms and puts it in an inescapable view
|
||||
* then lets out the focus by requesting focus to result_episodes
|
||||
*/
|
||||
|
||||
val hasEpisodes =
|
||||
!(resultEpisodes.adapter as? EpisodeAdapter?)?.cardList.isNullOrEmpty()
|
||||
/*val focus = activity?.currentFocus
|
||||
|
||||
if (hasEpisodes) {
|
||||
// Make it impossible to focus anywhere else!
|
||||
temporaryNoFocus.isFocusable = true
|
||||
temporaryNoFocus.requestFocus()
|
||||
}*/
|
||||
|
||||
(resultEpisodes.adapter as? EpisodeAdapter)?.updateList(episodes.value)
|
||||
|
||||
/* if (hasEpisodes) main {
|
||||
|
||||
delay(500)
|
||||
// This might make some people sad as it changes the focus when leaving an episode :(
|
||||
if(focus?.requestFocus() == true) {
|
||||
temporaryNoFocus.isFocusable = false
|
||||
return@main
|
||||
}
|
||||
temporaryNoFocus.isFocusable = false
|
||||
temporaryNoFocus.requestFocus()
|
||||
}
|
||||
|
||||
if (hasNoFocus())
|
||||
binding?.resultEpisodes?.requestFocus()*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
observeNullable(viewModel.page) { data ->
|
||||
if (data == null) return@observeNullable
|
||||
binding?.apply {
|
||||
when (data) {
|
||||
is Resource.Success -> {
|
||||
val d = data.value
|
||||
resultVpn.setText(d.vpnText)
|
||||
resultInfo.setText(d.metaText)
|
||||
resultNoEpisodes.setText(d.noEpisodesFoundText)
|
||||
resultTitle.setText(d.titleText)
|
||||
resultMetaSite.setText(d.apiName)
|
||||
resultMetaType.setText(d.typeText)
|
||||
resultMetaYear.setText(d.yearText)
|
||||
resultMetaDuration.setText(d.durationText)
|
||||
resultMetaRating.setText(d.ratingText)
|
||||
resultCastText.setText(d.actorsText)
|
||||
resultNextAiring.setText(d.nextAiringEpisode)
|
||||
resultNextAiringTime.setText(d.nextAiringDate)
|
||||
resultPoster.setImage(d.posterImage)
|
||||
resultDescription.setTextHtml(d.plotText)
|
||||
resultDescription.setOnClickListener { view ->
|
||||
view.context?.let { ctx ->
|
||||
val builder: AlertDialog.Builder =
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
builder.setMessage(d.plotText.asString(ctx).html())
|
||||
.setTitle(d.plotHeaderText.asString(ctx))
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
val error = listOf(
|
||||
R.drawable.profile_bg_dark_blue,
|
||||
R.drawable.profile_bg_blue,
|
||||
R.drawable.profile_bg_orange,
|
||||
R.drawable.profile_bg_pink,
|
||||
R.drawable.profile_bg_purple,
|
||||
R.drawable.profile_bg_red,
|
||||
R.drawable.profile_bg_teal
|
||||
).random()
|
||||
backgroundPoster.setImage(
|
||||
d.posterBackgroundImage ?: UiImage.Drawable(error),
|
||||
radius = 0,
|
||||
errorImageDrawable = error
|
||||
)
|
||||
|
||||
resultComingSoon.isVisible = d.comingSoon
|
||||
resultDataHolder.isGone = d.comingSoon
|
||||
UIHelper.populateChips(resultTag, d.tags)
|
||||
resultCastItems.isGone = d.actors.isNullOrEmpty()
|
||||
(resultCastItems.adapter as? ActorAdaptor)?.updateList(
|
||||
d.actors ?: emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
is Resource.Loading -> {
|
||||
|
||||
}
|
||||
|
||||
is Resource.Failure -> {
|
||||
resultErrorText.text =
|
||||
storedData.url.plus("\n") + data.errorString
|
||||
}
|
||||
}
|
||||
|
||||
resultFinishLoading.isVisible = data is Resource.Success
|
||||
|
||||
resultLoading.isVisible = data is Resource.Loading
|
||||
|
||||
resultLoadingError.isVisible = data is Resource.Failure
|
||||
//resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -19,6 +19,7 @@ import com.lagradost.cloudstream3.APIHolder.getId
|
|||
import com.lagradost.cloudstream3.APIHolder.unixTime
|
||||
import com.lagradost.cloudstream3.APIHolder.unixTimeMS
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.CommonActivity.getCastSession
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer
|
||||
|
@ -145,15 +146,18 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
|
|||
minute
|
||||
)
|
||||
}
|
||||
|
||||
hours > 0 -> txt(
|
||||
R.string.next_episode_time_hour_format,
|
||||
hours,
|
||||
minute
|
||||
)
|
||||
|
||||
minute > 0 -> txt(
|
||||
R.string.next_episode_time_min_format,
|
||||
minute
|
||||
)
|
||||
|
||||
else -> null
|
||||
}?.also {
|
||||
nextAiringEpisode = txt(R.string.next_episode_format, airing.episode)
|
||||
|
@ -305,6 +309,7 @@ fun SelectPopup.getOptions(context: Context): List<String> {
|
|||
is SelectPopup.SelectArray -> {
|
||||
this.options.map { it.first.asString(context) }
|
||||
}
|
||||
|
||||
is SelectPopup.SelectText -> options.map { it.asString(context) }
|
||||
}
|
||||
}
|
||||
|
@ -316,7 +321,7 @@ data class ExtractedTrailerData(
|
|||
|
||||
class ResultViewModel2 : ViewModel() {
|
||||
private var currentResponse: LoadResponse? = null
|
||||
|
||||
var EPISODE_RANGE_SIZE: Int = 20
|
||||
fun clear() {
|
||||
currentResponse = null
|
||||
_page.postValue(null)
|
||||
|
@ -337,7 +342,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
private var currentIndex: EpisodeIndexer? = null
|
||||
private var currentRange: EpisodeRange? = null
|
||||
private var currentShowFillers: Boolean = false
|
||||
private var currentRepo: APIRepository? = null
|
||||
var currentRepo: APIRepository? = null
|
||||
private var currentId: Int? = null
|
||||
private var fillers: Map<Int, Boolean> = emptyMap()
|
||||
private var generator: IGenerator? = null
|
||||
|
@ -352,17 +357,17 @@ class ResultViewModel2 : ViewModel() {
|
|||
MutableLiveData(null)
|
||||
val page: LiveData<Resource<ResultData>?> = _page
|
||||
|
||||
private val _episodes: MutableLiveData<ResourceSome<List<ResultEpisode>>> =
|
||||
MutableLiveData(ResourceSome.Loading())
|
||||
val episodes: LiveData<ResourceSome<List<ResultEpisode>>> = _episodes
|
||||
private val _episodes: MutableLiveData<Resource<List<ResultEpisode>>?> =
|
||||
MutableLiveData(Resource.Loading())
|
||||
val episodes: LiveData<Resource<List<ResultEpisode>>?> = _episodes
|
||||
|
||||
private val _movie: MutableLiveData<ResourceSome<Pair<UiText, ResultEpisode>>> =
|
||||
MutableLiveData(ResourceSome.None)
|
||||
val movie: LiveData<ResourceSome<Pair<UiText, ResultEpisode>>> = _movie
|
||||
private val _movie: MutableLiveData<Resource<Pair<UiText, ResultEpisode>>?> =
|
||||
MutableLiveData(null)
|
||||
val movie: LiveData<Resource<Pair<UiText, ResultEpisode>>?> = _movie
|
||||
|
||||
private val _episodesCountText: MutableLiveData<Some<UiText>> =
|
||||
MutableLiveData(Some.None)
|
||||
val episodesCountText: LiveData<Some<UiText>> = _episodesCountText
|
||||
private val _episodesCountText: MutableLiveData<UiText?> =
|
||||
MutableLiveData(null)
|
||||
val episodesCountText: LiveData<UiText?> = _episodesCountText
|
||||
|
||||
private val _trailers: MutableLiveData<List<ExtractedTrailerData>> =
|
||||
MutableLiveData(mutableListOf())
|
||||
|
@ -384,16 +389,16 @@ class ResultViewModel2 : ViewModel() {
|
|||
MutableLiveData(emptyList())
|
||||
val recommendations: LiveData<List<SearchResponse>> = _recommendations
|
||||
|
||||
private val _selectedRange: MutableLiveData<Some<UiText>> =
|
||||
MutableLiveData(Some.None)
|
||||
val selectedRange: LiveData<Some<UiText>> = _selectedRange
|
||||
private val _selectedRange: MutableLiveData<UiText?> =
|
||||
MutableLiveData(null)
|
||||
val selectedRange: LiveData<UiText?> = _selectedRange
|
||||
|
||||
private val _selectedSeason: MutableLiveData<Some<UiText>> =
|
||||
MutableLiveData(Some.None)
|
||||
val selectedSeason: LiveData<Some<UiText>> = _selectedSeason
|
||||
private val _selectedSeason: MutableLiveData<UiText?> =
|
||||
MutableLiveData(null)
|
||||
val selectedSeason: LiveData<UiText?> = _selectedSeason
|
||||
|
||||
private val _selectedDubStatus: MutableLiveData<Some<UiText>> = MutableLiveData(Some.None)
|
||||
val selectedDubStatus: LiveData<Some<UiText>> = _selectedDubStatus
|
||||
private val _selectedDubStatus: MutableLiveData<UiText?> = MutableLiveData(null)
|
||||
val selectedDubStatus: LiveData<UiText?> = _selectedDubStatus
|
||||
|
||||
private val _selectedRangeIndex: MutableLiveData<Int> =
|
||||
MutableLiveData(-1)
|
||||
|
@ -406,12 +411,12 @@ class ResultViewModel2 : ViewModel() {
|
|||
private val _selectedDubStatusIndex: MutableLiveData<Int> = MutableLiveData(-1)
|
||||
val selectedDubStatusIndex: LiveData<Int> = _selectedDubStatusIndex
|
||||
|
||||
private val _loadedLinks: MutableLiveData<Some<LinkProgress>> = MutableLiveData(Some.None)
|
||||
val loadedLinks: LiveData<Some<LinkProgress>> = _loadedLinks
|
||||
private val _loadedLinks: MutableLiveData<LinkProgress?> = MutableLiveData(null)
|
||||
val loadedLinks: LiveData<LinkProgress?> = _loadedLinks
|
||||
|
||||
private val _resumeWatching: MutableLiveData<Some<ResumeWatchingStatus>> =
|
||||
MutableLiveData(Some.None)
|
||||
val resumeWatching: LiveData<Some<ResumeWatchingStatus>> = _resumeWatching
|
||||
private val _resumeWatching: MutableLiveData<ResumeWatchingStatus?> =
|
||||
MutableLiveData(null)
|
||||
val resumeWatching: LiveData<ResumeWatchingStatus?> = _resumeWatching
|
||||
|
||||
private val _episodeSynopsis: MutableLiveData<String?> = MutableLiveData(null)
|
||||
val episodeSynopsis: LiveData<String?> = _episodeSynopsis
|
||||
|
@ -421,8 +426,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
companion object {
|
||||
const val TAG = "RVM2"
|
||||
private const val EPISODE_RANGE_SIZE = 20
|
||||
private const val EPISODE_RANGE_OVERLOAD = 30
|
||||
//private const val EPISODE_RANGE_SIZE = 20
|
||||
//private const val EPISODE_RANGE_OVERLOAD = 30
|
||||
|
||||
private fun List<SeasonData>?.getSeason(season: Int?): SeasonData? {
|
||||
if (season == null) return null
|
||||
|
@ -432,6 +437,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
fun updateWatchStatus(currentResponse: LoadResponse, status: WatchType) {
|
||||
val currentId = currentResponse.getId()
|
||||
|
||||
val currentWatchType = getResultWatchState(currentId)
|
||||
|
||||
DataStoreHelper.setResultWatchState(currentId, status.internalId)
|
||||
val current = DataStoreHelper.getBookmarkedData(currentId)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
@ -449,6 +456,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
currentResponse.year
|
||||
)
|
||||
)
|
||||
if (currentWatchType != status) {
|
||||
MainActivity.bookmarksUpdatedEvent(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterName(name: String?): String? {
|
||||
|
@ -467,12 +477,16 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
|
||||
private fun getRanges(allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>): Map<EpisodeIndexer, List<EpisodeRange>> {
|
||||
private fun getRanges(
|
||||
allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>,
|
||||
EPISODE_RANGE_SIZE: Int
|
||||
): Map<EpisodeIndexer, List<EpisodeRange>> {
|
||||
return allEpisodes.keys.mapNotNull { index ->
|
||||
val episodes =
|
||||
allEpisodes[index] ?: return@mapNotNull null // this should never happened
|
||||
|
||||
// fast case
|
||||
val EPISODE_RANGE_OVERLOAD = EPISODE_RANGE_SIZE + 10
|
||||
if (episodes.size <= EPISODE_RANGE_OVERLOAD) {
|
||||
return@mapNotNull index to listOf(
|
||||
EpisodeRange(
|
||||
|
@ -744,7 +758,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
if (currentLinks.isEmpty()) {
|
||||
main {
|
||||
showToast(
|
||||
activity,
|
||||
R.string.no_links_found_toast,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
|
@ -753,7 +766,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
} else {
|
||||
main {
|
||||
showToast(
|
||||
activity,
|
||||
R.string.download_started,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
|
@ -800,8 +812,8 @@ class ResultViewModel2 : ViewModel() {
|
|||
private val _watchStatus: MutableLiveData<WatchType> = MutableLiveData(WatchType.NONE)
|
||||
val watchStatus: LiveData<WatchType> get() = _watchStatus
|
||||
|
||||
private val _selectPopup: MutableLiveData<Some<SelectPopup>> = MutableLiveData(Some.None)
|
||||
val selectPopup: LiveData<Some<SelectPopup>> get() = _selectPopup
|
||||
private val _selectPopup: MutableLiveData<SelectPopup?> = MutableLiveData(null)
|
||||
val selectPopup: LiveData<SelectPopup?> = _selectPopup
|
||||
|
||||
|
||||
fun updateWatchStatus(status: WatchType) {
|
||||
|
@ -885,23 +897,22 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
fun cancelLinks() {
|
||||
println("called::cancelLinks")
|
||||
currentLoadLinkJob?.cancel()
|
||||
currentLoadLinkJob = null
|
||||
_loadedLinks.postValue(Some.None)
|
||||
_loadedLinks.postValue(null)
|
||||
}
|
||||
|
||||
private fun postPopup(text: UiText, options: List<UiText>, callback: suspend (Int?) -> Unit) {
|
||||
_selectPopup.postValue(
|
||||
some(SelectPopup.SelectText(
|
||||
SelectPopup.SelectText(
|
||||
text,
|
||||
options
|
||||
) { value ->
|
||||
viewModelScope.launchSafe {
|
||||
_selectPopup.postValue(Some.None)
|
||||
_selectPopup.postValue(null)
|
||||
callback.invoke(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -912,15 +923,15 @@ class ResultViewModel2 : ViewModel() {
|
|||
callback: suspend (Int?) -> Unit
|
||||
) {
|
||||
_selectPopup.postValue(
|
||||
some(SelectPopup.SelectArray(
|
||||
SelectPopup.SelectArray(
|
||||
text,
|
||||
options,
|
||||
) { value ->
|
||||
viewModelScope.launchSafe {
|
||||
_selectPopup.value = Some.None
|
||||
_selectPopup.postValue(null)
|
||||
callback.invoke(value)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -988,7 +999,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
val subs: MutableSet<SubtitleData> = mutableSetOf()
|
||||
fun updatePage() {
|
||||
if (isVisible && isActive) {
|
||||
_loadedLinks.postValue(some(LinkProgress(links.size, subs.size)))
|
||||
_loadedLinks.postValue(LinkProgress(links.size, subs.size))
|
||||
}
|
||||
}
|
||||
try {
|
||||
|
@ -1005,7 +1016,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
} finally {
|
||||
_loadedLinks.postValue(Some.None)
|
||||
_loadedLinks.postValue(null)
|
||||
}
|
||||
|
||||
return LinkLoadingResult(sortUrls(links), sortSubs(subs))
|
||||
|
@ -1027,9 +1038,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
logError(t)
|
||||
main {
|
||||
if (t is ActivityNotFoundException) {
|
||||
showToast(activity, txt(R.string.app_not_found_error), Toast.LENGTH_LONG)
|
||||
showToast(txt(R.string.app_not_found_error), Toast.LENGTH_LONG)
|
||||
} else {
|
||||
showToast(activity, t.toString(), Toast.LENGTH_LONG)
|
||||
showToast(t.toString(), Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1138,9 +1149,9 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
|
||||
fun handleAction(activity: Activity?, click: EpisodeClickEvent) =
|
||||
fun handleAction(click: EpisodeClickEvent) =
|
||||
viewModelScope.launchSafe {
|
||||
handleEpisodeClickEvent(activity, click)
|
||||
handleEpisodeClickEvent(click)
|
||||
}
|
||||
|
||||
data class ExternalApp(
|
||||
|
@ -1170,7 +1181,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
_episodeSynopsis.postValue(null)
|
||||
}
|
||||
|
||||
private suspend fun handleEpisodeClickEvent(activity: Activity?, click: EpisodeClickEvent) {
|
||||
private suspend fun handleEpisodeClickEvent(click: EpisodeClickEvent) {
|
||||
when (click.action) {
|
||||
ACTION_SHOW_OPTIONS -> {
|
||||
val options = mutableListOf<Pair<UiText, Int>>()
|
||||
|
@ -1228,27 +1239,26 @@ class ResultViewModel2 : ViewModel() {
|
|||
options
|
||||
) { result ->
|
||||
handleEpisodeClickEvent(
|
||||
activity,
|
||||
click.copy(action = result ?: return@postPopup)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_CLICK_DEFAULT -> {
|
||||
activity?.let { ctx ->
|
||||
if (ctx.isConnectedToChromecast()) {
|
||||
handleEpisodeClickEvent(
|
||||
activity,
|
||||
click.copy(action = ACTION_CHROME_CAST_EPISODE)
|
||||
)
|
||||
} else {
|
||||
val action = getPlayerAction(ctx)
|
||||
handleEpisodeClickEvent(
|
||||
activity,
|
||||
click.copy(action = action)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_SHOW_DESCRIPTION -> {
|
||||
_episodeSynopsis.postValue(click.data.description)
|
||||
}
|
||||
|
@ -1280,15 +1290,16 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
showToast(
|
||||
activity,
|
||||
R.string.download_started,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_SHOW_TOAST -> {
|
||||
showToast(activity, R.string.play_episode_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.play_episode_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_EPISODE -> {
|
||||
val response = currentResponse ?: return
|
||||
downloadEpisode(
|
||||
|
@ -1303,6 +1314,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
response.url
|
||||
)
|
||||
}
|
||||
|
||||
ACTION_DOWNLOAD_MIRROR -> {
|
||||
val response = currentResponse ?: return
|
||||
acquireSingleLink(
|
||||
|
@ -1326,12 +1338,12 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
}
|
||||
showToast(
|
||||
activity,
|
||||
R.string.download_started,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_RELOAD_EPISODE -> {
|
||||
ioSafe {
|
||||
loadLinks(
|
||||
|
@ -1342,6 +1354,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_CHROME_CAST_MIRROR -> {
|
||||
acquireSingleLink(
|
||||
click.data,
|
||||
|
@ -1351,6 +1364,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
startChromecast(activity, click.data, result.links, result.subs, index)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_BROWSER -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
|
@ -1364,6 +1378,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_COPY_LINK -> {
|
||||
acquireSingleLink(
|
||||
click.data,
|
||||
|
@ -1377,16 +1392,18 @@ class ResultViewModel2 : ViewModel() {
|
|||
val link = result.links[index]
|
||||
val clip = ClipData.newPlainText(link.name, link.url)
|
||||
serviceClipboard.setPrimaryClip(clip)
|
||||
showToast(act, R.string.copy_link_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.copy_link_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_CHROME_CAST_EPISODE -> {
|
||||
startChromecast(activity, click.data)
|
||||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_VLC_PLAYER -> {
|
||||
loadLinks(click.data, isVisible = true, isCasting = true) { links ->
|
||||
if (links.links.isEmpty()) {
|
||||
showToast(activity, R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
|
||||
return@loadLinks
|
||||
}
|
||||
|
||||
|
@ -1397,6 +1414,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_WEB_VIDEO -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
|
@ -1413,6 +1431,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
result.subs
|
||||
)
|
||||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink(
|
||||
click.data,
|
||||
isCasting = true,
|
||||
|
@ -1428,6 +1447,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
result.subs
|
||||
)
|
||||
}
|
||||
|
||||
ACTION_PLAY_EPISODE_IN_PLAYER -> {
|
||||
val data = currentResponse?.syncData?.toList() ?: emptyList()
|
||||
val list =
|
||||
|
@ -1448,6 +1468,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
ACTION_MARK_AS_WATCHED -> {
|
||||
val isWatched =
|
||||
DataStoreHelper.getVideoWatchState(click.data.id) == VideoWatchState.Watched
|
||||
|
@ -1487,13 +1508,14 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
val realRecommendations = ArrayList<SearchResponse>()
|
||||
val apiNames = apis.filter {
|
||||
it.name.contains("gogoanime", true) ||
|
||||
it.name.contains("9anime", true)
|
||||
}.map {
|
||||
it.name
|
||||
val apiNames = synchronized(apis) {
|
||||
apis.filter {
|
||||
it.name.contains("gogoanime", true) ||
|
||||
it.name.contains("9anime", true)
|
||||
}.map {
|
||||
it.name
|
||||
}
|
||||
}
|
||||
|
||||
meta.recommendations?.forEach { rec ->
|
||||
apiNames.forEach { name ->
|
||||
realRecommendations.add(rec.copy(apiName = name))
|
||||
|
@ -1672,10 +1694,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
private fun postMovie() {
|
||||
val response = currentResponse
|
||||
_episodes.postValue(ResourceSome.None)
|
||||
_episodes.postValue(null)
|
||||
|
||||
if (response == null) {
|
||||
_movie.postValue(ResourceSome.None)
|
||||
_movie.postValue(null)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1692,11 +1714,11 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
)
|
||||
val data = getMovie()
|
||||
_episodes.postValue(ResourceSome.None)
|
||||
_episodes.postValue(null)
|
||||
if (text == null || data == null) {
|
||||
_movie.postValue(ResourceSome.None)
|
||||
_movie.postValue(null)
|
||||
} else {
|
||||
_movie.postValue(ResourceSome.Success(text to data))
|
||||
_movie.postValue(Resource.Success(text to data))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1705,14 +1727,14 @@ class ResultViewModel2 : ViewModel() {
|
|||
postMovie()
|
||||
} else {
|
||||
_episodes.postValue(
|
||||
ResourceSome.Success(
|
||||
Resource.Success(
|
||||
getEpisodes(
|
||||
currentIndex ?: return,
|
||||
currentRange ?: return
|
||||
)
|
||||
)
|
||||
)
|
||||
_movie.postValue(ResourceSome.None)
|
||||
_movie.postValue(null)
|
||||
}
|
||||
postResume()
|
||||
}
|
||||
|
@ -1755,14 +1777,14 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
val size = currentEpisodes[indexer]?.size
|
||||
_episodesCountText.postValue(
|
||||
some(
|
||||
if (isMovie) null else
|
||||
txt(
|
||||
R.string.episode_format,
|
||||
size,
|
||||
txt(if (size == 1) R.string.episode else R.string.episodes),
|
||||
)
|
||||
)
|
||||
|
||||
if (isMovie) null else
|
||||
txt(
|
||||
R.string.episode_format,
|
||||
size,
|
||||
txt(if (size == 1) R.string.episode else R.string.episodes),
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
_selectedSeasonIndex.postValue(
|
||||
|
@ -1770,29 +1792,29 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
|
||||
_selectedSeason.postValue(
|
||||
some(
|
||||
if (isMovie || currentSeasons.size <= 1) null else
|
||||
when (indexer.season) {
|
||||
0 -> txt(R.string.no_season)
|
||||
else -> {
|
||||
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
|
||||
val seasonData = seasonNames.getSeason(indexer.season)
|
||||
|
||||
// If displaySeason is null then only show the name!
|
||||
if (seasonData?.name != null && seasonData.displaySeason == null) {
|
||||
txt(seasonData.name)
|
||||
} else {
|
||||
val suffix = seasonData?.name?.let { " $it" } ?: ""
|
||||
txt(
|
||||
R.string.season_format,
|
||||
txt(R.string.season),
|
||||
seasonData?.displaySeason ?: indexer.season,
|
||||
suffix
|
||||
)
|
||||
}
|
||||
if (isMovie || currentSeasons.size <= 1) null else
|
||||
when (indexer.season) {
|
||||
0 -> txt(R.string.no_season)
|
||||
else -> {
|
||||
val seasonNames = (currentResponse as? EpisodeResponse)?.seasonNames
|
||||
val seasonData = seasonNames.getSeason(indexer.season)
|
||||
|
||||
// If displaySeason is null then only show the name!
|
||||
if (seasonData?.name != null && seasonData.displaySeason == null) {
|
||||
txt(seasonData.name)
|
||||
} else {
|
||||
val suffix = seasonData?.name?.let { " $it" } ?: ""
|
||||
txt(
|
||||
R.string.season_format,
|
||||
txt(R.string.season),
|
||||
seasonData?.displaySeason ?: indexer.season,
|
||||
suffix
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
_selectedRangeIndex.postValue(
|
||||
|
@ -1800,13 +1822,13 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
|
||||
_selectedRange.postValue(
|
||||
some(
|
||||
if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) {
|
||||
txt(R.string.episodes_range, range.startEpisode, range.endEpisode)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
|
||||
if (isMovie) null else if ((currentRanges[indexer]?.size ?: 0) > 1) {
|
||||
txt(R.string.episodes_range, range.startEpisode, range.endEpisode)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
)
|
||||
|
||||
_selectedDubStatusIndex.postValue(
|
||||
|
@ -1814,10 +1836,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
|
||||
_selectedDubStatus.postValue(
|
||||
some(
|
||||
if (isMovie || currentDubStatus.size <= 1) null else
|
||||
txt(indexer.dubStatus)
|
||||
)
|
||||
|
||||
if (isMovie || currentDubStatus.size <= 1) null else
|
||||
txt(indexer.dubStatus)
|
||||
|
||||
)
|
||||
|
||||
currentId?.let { id ->
|
||||
|
@ -1851,7 +1873,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
|
||||
}
|
||||
}*/
|
||||
_episodes.postValue(ResourceSome.Success(ret))
|
||||
_episodes.postValue(Resource.Success(ret))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1869,7 +1891,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
private suspend fun postEpisodes(loadResponse: LoadResponse, updateFillers: Boolean) {
|
||||
_episodes.postValue(ResourceSome.Loading())
|
||||
_episodes.postValue(Resource.Loading())
|
||||
|
||||
val mainId = loadResponse.getId()
|
||||
currentId = mainId
|
||||
|
@ -1924,6 +1946,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
episodes
|
||||
}
|
||||
|
||||
is TvSeriesLoadResponse -> {
|
||||
val episodes: MutableMap<EpisodeIndexer, MutableList<ResultEpisode>> =
|
||||
mutableMapOf()
|
||||
|
@ -1968,6 +1991,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
episodes
|
||||
}
|
||||
|
||||
is MovieLoadResponse -> {
|
||||
singleMap(
|
||||
buildResultEpisode(
|
||||
|
@ -1989,6 +2013,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
is LiveStreamLoadResponse -> {
|
||||
singleMap(
|
||||
buildResultEpisode(
|
||||
|
@ -2010,6 +2035,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
is TorrentLoadResponse -> {
|
||||
singleMap(
|
||||
buildResultEpisode(
|
||||
|
@ -2031,6 +2057,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
mapOf()
|
||||
}
|
||||
|
@ -2066,7 +2093,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
currentEpisodes = allEpisodes
|
||||
val ranges = getRanges(allEpisodes)
|
||||
val ranges = getRanges(allEpisodes, EPISODE_RANGE_SIZE)
|
||||
currentRanges = ranges
|
||||
|
||||
|
||||
|
@ -2088,7 +2115,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
fun postResume() {
|
||||
_resumeWatching.postValue(some(resume()))
|
||||
_resumeWatching.postValue(resume())
|
||||
}
|
||||
|
||||
private fun resume(): ResumeWatchingStatus? {
|
||||
|
@ -2186,7 +2213,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
for (ep in currentRange) {
|
||||
if (ep.getWatchProgress() > 0.9) continue
|
||||
handleAction(
|
||||
activity,
|
||||
EpisodeClickEvent(
|
||||
getPlayerAction(activity),
|
||||
ep
|
||||
|
@ -2196,6 +2222,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
START_ACTION_LOAD_EP -> {
|
||||
val all = currentEpisodes.values.flatten()
|
||||
val episode =
|
||||
|
@ -2206,7 +2233,6 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
?: return@launchSafe
|
||||
handleAction(
|
||||
activity,
|
||||
EpisodeClickEvent(
|
||||
getPlayerAction(activity),
|
||||
episode
|
||||
|
@ -2227,7 +2253,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
) =
|
||||
ioSafe {
|
||||
_page.postValue(Resource.Loading(url))
|
||||
_episodes.postValue(ResourceSome.Loading())
|
||||
_episodes.postValue(Resource.Loading())
|
||||
|
||||
preferDubStatus = dubStatus
|
||||
currentShowFillers = showFillers
|
||||
|
@ -2271,6 +2297,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
is Resource.Failure -> {
|
||||
_page.postValue(data)
|
||||
}
|
||||
|
||||
is Resource.Success -> {
|
||||
if (!isActive) return@ioSafe
|
||||
val loadResponse = ioWork {
|
||||
|
@ -2307,6 +2334,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
if (!isActive) return@ioSafe
|
||||
handleAutoStart(activity, autostart)
|
||||
}
|
||||
|
||||
is Resource.Loading -> {
|
||||
debugException { "Invalid load result" }
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -33,6 +33,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSearchBinding
|
||||
import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
|
@ -56,8 +58,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||
import kotlinx.android.synthetic.main.fragment_search.*
|
||||
import kotlinx.android.synthetic.main.tvtypes_chips.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
const val SEARCH_PREF_TAGS = "search_pref_tags"
|
||||
|
@ -89,6 +89,7 @@ class SearchFragment : Fragment() {
|
|||
|
||||
private val searchViewModel: SearchViewModel by activityViewModels()
|
||||
private var bottomSheetDialog: BottomSheetDialog? = null
|
||||
var binding: FragmentSearchBinding? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -99,18 +100,21 @@ class SearchFragment : Fragment() {
|
|||
WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
|
||||
)
|
||||
bottomSheetDialog?.ownShow()
|
||||
return inflater.inflate(
|
||||
if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search,
|
||||
container,
|
||||
false
|
||||
)
|
||||
|
||||
val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search
|
||||
|
||||
val root = inflater.inflate(layout, container, false)
|
||||
// TODO TRYCATCH
|
||||
binding = FragmentSearchBinding.bind(root)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
private fun fixGrid() {
|
||||
activity?.getSpanCount()?.let {
|
||||
currentSpan = it
|
||||
}
|
||||
search_autofit_results.spanCount = currentSpan
|
||||
binding?.searchAutofitResults?.spanCount = currentSpan
|
||||
currentSpan = currentSpan
|
||||
HomeFragment.configEvent.invoke(currentSpan)
|
||||
}
|
||||
|
@ -123,6 +127,7 @@ class SearchFragment : Fragment() {
|
|||
override fun onDestroyView() {
|
||||
hideKeyboard()
|
||||
bottomSheetDialog?.ownHide()
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
|
@ -181,7 +186,7 @@ class SearchFragment : Fragment() {
|
|||
searchViewModel.reloadRepos()
|
||||
context?.filterProviderByPreferredMedia()?.let { validAPIs ->
|
||||
bindChips(
|
||||
home_select_group,
|
||||
binding?.tvtypesChipsScroll?.tvtypesChips,
|
||||
selectedSearchTypes,
|
||||
validAPIs.flatMap { api -> api.supportedTypes }.distinct()
|
||||
) { list ->
|
||||
|
@ -189,7 +194,7 @@ class SearchFragment : Fragment() {
|
|||
setKey(SEARCH_PREF_TAGS, selectedSearchTypes)
|
||||
selectedSearchTypes.clear()
|
||||
selectedSearchTypes.addAll(list)
|
||||
search(main_search?.query?.toString())
|
||||
search(binding?.mainSearch?.query?.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,24 +204,27 @@ class SearchFragment : Fragment() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
context?.fixPaddingStatusbar(searchRoot)
|
||||
fixPaddingStatusbar(binding?.searchRoot)
|
||||
fixGrid()
|
||||
reloadRepos()
|
||||
|
||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? = activity?.let {
|
||||
SearchAdapter(
|
||||
ArrayList(),
|
||||
search_autofit_results,
|
||||
) { callback ->
|
||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
}
|
||||
binding?.apply {
|
||||
val adapter: RecyclerView.Adapter<RecyclerView.ViewHolder>? =
|
||||
SearchAdapter(
|
||||
ArrayList(),
|
||||
searchAutofitResults,
|
||||
) { callback ->
|
||||
SearchHelper.handleSearchClickCallback(callback)
|
||||
}
|
||||
|
||||
|
||||
searchAutofitResults.adapter = adapter
|
||||
searchLoadingBar.alpha = 0f
|
||||
}
|
||||
|
||||
search_autofit_results.adapter = adapter
|
||||
search_loading_bar.alpha = 0f
|
||||
|
||||
val searchExitIcon =
|
||||
main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
|
||||
// val searchMagIcon =
|
||||
// main_search.findViewById<ImageView>(androidx.appcompat.R.id.search_mag_icon)
|
||||
//searchMagIcon.scaleX = 0.65f
|
||||
|
@ -230,7 +238,7 @@ class SearchFragment : Fragment() {
|
|||
)!!.toMutableSet()
|
||||
}
|
||||
|
||||
search_filter.setOnClickListener { searchView ->
|
||||
binding?.searchFilter?.setOnClickListener { searchView ->
|
||||
searchView?.context?.let { ctx ->
|
||||
val validAPIs = ctx.filterProviderByPreferredMedia(hasHomePageIsRequired = false)
|
||||
var currentValidApis = listOf<MainAPI>()
|
||||
|
@ -241,7 +249,13 @@ class SearchFragment : Fragment() {
|
|||
BottomSheetDialog(ctx)
|
||||
|
||||
builder.behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
builder.setContentView(R.layout.home_select_mainpage)
|
||||
|
||||
val binding: HomeSelectMainpageBinding = HomeSelectMainpageBinding.inflate(
|
||||
builder.layoutInflater,
|
||||
null,
|
||||
false
|
||||
)
|
||||
builder.setContentView(binding.root)
|
||||
builder.show()
|
||||
builder.let { dialog ->
|
||||
val isMultiLang = ctx.getApiProviderLangSettings().let { set ->
|
||||
|
@ -303,7 +317,7 @@ class SearchFragment : Fragment() {
|
|||
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||
|
||||
bindChips(
|
||||
dialog.home_select_group,
|
||||
binding.tvtypesChipsScroll.tvtypesChips,
|
||||
selectedSearchTypes,
|
||||
TvType.values().toList()
|
||||
) { list ->
|
||||
|
@ -343,15 +357,15 @@ class SearchFragment : Fragment() {
|
|||
?: mutableListOf(TvType.Movie, TvType.TvSeries)
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
search_filter.isFocusable = true
|
||||
search_filter.isFocusableInTouchMode = true
|
||||
binding?.searchFilter?.isFocusable = true
|
||||
binding?.searchFilter?.isFocusableInTouchMode = true
|
||||
}
|
||||
|
||||
main_search.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
search(query)
|
||||
|
||||
main_search?.let {
|
||||
binding?.mainSearch?.let {
|
||||
hideKeyboard(it)
|
||||
}
|
||||
|
||||
|
@ -365,17 +379,17 @@ class SearchFragment : Fragment() {
|
|||
searchViewModel.clearSearch()
|
||||
searchViewModel.updateHistory()
|
||||
}
|
||||
|
||||
search_history_holder?.isVisible = showHistory
|
||||
|
||||
search_master_recycler?.isVisible = !showHistory && isAdvancedSearch
|
||||
search_autofit_results?.isVisible = !showHistory && !isAdvancedSearch
|
||||
binding?.apply {
|
||||
searchHistoryHolder.isVisible = showHistory
|
||||
searchMasterRecycler.isVisible = !showHistory && isAdvancedSearch
|
||||
searchAutofitResults.isVisible = !showHistory && !isAdvancedSearch
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
search_clear_call_history?.setOnClickListener {
|
||||
binding?.searchClearCallHistory?.setOnClickListener {
|
||||
activity?.let { ctx ->
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(ctx)
|
||||
val dialogClickListener =
|
||||
|
@ -409,8 +423,8 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
observe(searchViewModel.currentHistory) { list ->
|
||||
search_clear_call_history?.isVisible = list.isNotEmpty()
|
||||
(search_history_recycler.adapter as? SearchHistoryAdaptor?)?.updateList(list)
|
||||
binding?.searchClearCallHistory?.isVisible = list.isNotEmpty()
|
||||
(binding?.searchHistoryRecycler?.adapter as? SearchHistoryAdaptor?)?.updateList(list)
|
||||
}
|
||||
|
||||
searchViewModel.updateHistory()
|
||||
|
@ -420,20 +434,20 @@ class SearchFragment : Fragment() {
|
|||
is Resource.Success -> {
|
||||
it.value.let { data ->
|
||||
if (data.isNotEmpty()) {
|
||||
(search_autofit_results?.adapter as? SearchAdapter)?.updateList(data)
|
||||
(binding?.searchAutofitResults?.adapter as? SearchAdapter)?.updateList(data)
|
||||
}
|
||||
}
|
||||
searchExitIcon.alpha = 1f
|
||||
search_loading_bar.alpha = 0f
|
||||
searchExitIcon?.alpha = 1f
|
||||
binding?.searchLoadingBar?.alpha = 0f
|
||||
}
|
||||
is Resource.Failure -> {
|
||||
// Toast.makeText(activity, "Server error", Toast.LENGTH_LONG).show()
|
||||
searchExitIcon.alpha = 1f
|
||||
search_loading_bar.alpha = 0f
|
||||
searchExitIcon?.alpha = 1f
|
||||
binding?.searchLoadingBar?.alpha = 0f
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
searchExitIcon.alpha = 0f
|
||||
search_loading_bar.alpha = 1f
|
||||
searchExitIcon?.alpha = 0f
|
||||
binding?.searchLoadingBar?.alpha = 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -443,7 +457,7 @@ class SearchFragment : Fragment() {
|
|||
try {
|
||||
// https://stackoverflow.com/questions/6866238/concurrent-modification-exception-adding-to-an-arraylist
|
||||
listLock.lock()
|
||||
(search_master_recycler?.adapter as ParentItemAdapter?)?.apply {
|
||||
(binding?.searchMasterRecycler?.adapter as ParentItemAdapter?)?.apply {
|
||||
val newItems = list.map { ongoing ->
|
||||
val dataList =
|
||||
if (ongoing.data is Resource.Success) ongoing.data.value else ArrayList()
|
||||
|
@ -477,7 +491,7 @@ class SearchFragment : Fragment() {
|
|||
|
||||
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
||||
ParentItemAdapter(mutableListOf(), { callback ->
|
||||
SearchHelper.handleSearchClickCallback(activity, callback)
|
||||
SearchHelper.handleSearchClickCallback(callback)
|
||||
}, { item ->
|
||||
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
|
||||
bottomSheetDialog = null
|
||||
|
@ -490,8 +504,8 @@ class SearchFragment : Fragment() {
|
|||
SEARCH_HISTORY_OPEN -> {
|
||||
searchViewModel.clearSearch()
|
||||
if (searchItem.type.isNotEmpty())
|
||||
updateChips(home_select_group, searchItem.type.toMutableList())
|
||||
main_search?.setQuery(searchItem.searchText, true)
|
||||
updateChips(binding?.tvtypesChipsScroll?.tvtypesChips, searchItem.type.toMutableList())
|
||||
binding?.mainSearch?.setQuery(searchItem.searchText, true)
|
||||
}
|
||||
SEARCH_HISTORY_REMOVE -> {
|
||||
removeKey(SEARCH_HISTORY_KEY, searchItem.key)
|
||||
|
@ -503,20 +517,23 @@ class SearchFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
search_history_recycler?.adapter = historyAdapter
|
||||
search_history_recycler?.layoutManager = GridLayoutManager(context, 1)
|
||||
binding?.apply {
|
||||
searchHistoryRecycler.adapter = historyAdapter
|
||||
searchHistoryRecycler.layoutManager = GridLayoutManager(context, 1)
|
||||
|
||||
search_master_recycler?.adapter = masterAdapter
|
||||
search_master_recycler?.layoutManager = GridLayoutManager(context, 1)
|
||||
searchMasterRecycler.adapter = masterAdapter
|
||||
searchMasterRecycler.layoutManager = GridLayoutManager(context, 1)
|
||||
|
||||
// Automatically search the specified query, this allows the app search to launch from intent
|
||||
arguments?.getString(SEARCH_QUERY)?.let { query ->
|
||||
if (query.isBlank()) return@let
|
||||
main_search?.setQuery(query, true)
|
||||
// Clear the query as to not make it request the same query every time the page is opened
|
||||
arguments?.putString(SEARCH_QUERY, null)
|
||||
// Automatically search the specified query, this allows the app search to launch from intent
|
||||
arguments?.getString(SEARCH_QUERY)?.let { query ->
|
||||
if (query.isBlank()) return@let
|
||||
mainSearch.setQuery(query, true)
|
||||
// Clear the query as to not make it request the same query every time the page is opened
|
||||
arguments?.putString(SEARCH_QUERY, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// SubtitlesFragment.push(activity)
|
||||
//searchViewModel.search("iron man")
|
||||
//(activity as AppCompatActivity).loadResult("https://shiro.is/overlord-dubbed", "overlord-dubbed", "Shiro")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,8 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import kotlinx.android.synthetic.main.search_history_item.view.*
|
||||
import com.lagradost.cloudstream3.databinding.AccountSingleBinding
|
||||
import com.lagradost.cloudstream3.databinding.SearchHistoryItemBinding
|
||||
|
||||
data class SearchHistoryItem(
|
||||
@JsonProperty("searchedAt") val searchedAt: Long,
|
||||
|
@ -34,8 +35,7 @@ class SearchHistoryAdaptor(
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return CardViewHolder(
|
||||
LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.search_history_item, parent, false),
|
||||
SearchHistoryItemBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
clickCallback,
|
||||
)
|
||||
}
|
||||
|
@ -65,22 +65,24 @@ class SearchHistoryAdaptor(
|
|||
|
||||
class CardViewHolder
|
||||
constructor(
|
||||
itemView: View,
|
||||
val binding: SearchHistoryItemBinding,
|
||||
private val clickCallback: (SearchHistoryCallback) -> Unit,
|
||||
) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
private val removeButton: ImageView = itemView.home_history_remove
|
||||
private val openButton: View = itemView.home_history_tab
|
||||
private val title: TextView = itemView.home_history_title
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
// private val removeButton: ImageView = itemView.home_history_remove
|
||||
// private val openButton: View = itemView.home_history_tab
|
||||
// private val title: TextView = itemView.home_history_title
|
||||
|
||||
fun bind(card: SearchHistoryItem) {
|
||||
title.text = card.searchText
|
||||
binding.apply {
|
||||
homeHistoryTitle.text = card.searchText
|
||||
|
||||
removeButton.setOnClickListener {
|
||||
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE))
|
||||
}
|
||||
openButton.setOnClickListener {
|
||||
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN))
|
||||
homeHistoryRemove.setOnClickListener {
|
||||
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_REMOVE))
|
||||
}
|
||||
homeHistoryTab.setOnClickListener {
|
||||
clickCallback.invoke(SearchHistoryCallback(card, SEARCH_HISTORY_OPEN))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package com.lagradost.cloudstream3.ui.search
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.ProgressBar
|
||||
|
@ -10,14 +9,20 @@ import androidx.cardview.widget.CardView
|
|||
import androidx.core.view.isVisible
|
||||
import androidx.palette.graphics.Palette
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.AnimeSearchResponse
|
||||
import com.lagradost.cloudstream3.DubStatus
|
||||
import com.lagradost.cloudstream3.LiveSearchResponse
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.SearchQuality
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.isMovieType
|
||||
import com.lagradost.cloudstream3.syncproviders.SyncAPI
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.home_result_grid.view.*
|
||||
|
||||
object SearchResultBuilder {
|
||||
private val showCache: MutableMap<String, Boolean> = mutableMapOf()
|
||||
|
@ -45,19 +50,21 @@ object SearchResultBuilder {
|
|||
nextFocusDown: Int? = null,
|
||||
colorCallback : ((Palette) -> Unit)? = null
|
||||
) {
|
||||
val cardView: ImageView = itemView.imageView
|
||||
val cardText: TextView? = itemView.imageText
|
||||
val cardView: ImageView = itemView.findViewById(R.id.imageView)
|
||||
val cardText: TextView? = itemView.findViewById(R.id.imageText)
|
||||
|
||||
val textIsDub: TextView? = itemView.text_is_dub
|
||||
val textIsSub: TextView? = itemView.text_is_sub
|
||||
val textFlag: TextView? = itemView.text_flag
|
||||
val textQuality: TextView? = itemView.text_quality
|
||||
val shadow: View? = itemView.title_shadow
|
||||
val textIsDub: TextView? = itemView.findViewById(R.id.text_is_dub)
|
||||
val textIsSub: TextView? = itemView.findViewById(R.id.text_is_sub)
|
||||
val textFlag: TextView? = itemView.findViewById(R.id.text_flag)
|
||||
val rating: TextView? = itemView.findViewById(R.id.text_rating)
|
||||
|
||||
val bg: CardView = itemView.background_card
|
||||
val textQuality: TextView? = itemView.findViewById(R.id.text_quality)
|
||||
val shadow: View? = itemView.findViewById(R.id.title_shadow)
|
||||
|
||||
val bar: ProgressBar? = itemView.watchProgress
|
||||
val playImg: ImageView? = itemView.search_item_download_play
|
||||
val bg: CardView = itemView.findViewById(R.id.background_card)
|
||||
|
||||
val bar: ProgressBar? = itemView.findViewById(R.id.watchProgress)
|
||||
val playImg: ImageView? = itemView.findViewById(R.id.search_item_download_play)
|
||||
|
||||
// Do logic
|
||||
|
||||
|
@ -66,12 +73,25 @@ object SearchResultBuilder {
|
|||
textIsDub?.isVisible = false
|
||||
textIsSub?.isVisible = false
|
||||
textFlag?.isVisible = false
|
||||
rating?.isVisible = false
|
||||
|
||||
val showSub = showCache[textIsDub?.context?.getString(R.string.show_sub_key)] ?: false
|
||||
val showDub = showCache[textIsDub?.context?.getString(R.string.show_dub_key)] ?: false
|
||||
val showTitle = showCache[cardText?.context?.getString(R.string.show_title_key)] ?: false
|
||||
val showHd = showCache[textQuality?.context?.getString(R.string.show_hd_key)] ?: false
|
||||
|
||||
if(card is SyncAPI.LibraryItem) {
|
||||
val showRating = (card.personalRating ?: 0) != 0
|
||||
rating?.isVisible = showRating
|
||||
if (showRating) {
|
||||
// We want to show 8.5 but not 8.0 hence the replace
|
||||
val ratingText = ((card.personalRating ?: 0).toDouble() / 10).toString()
|
||||
.replace(".0", "")
|
||||
|
||||
rating?.text = ratingText
|
||||
}
|
||||
}
|
||||
|
||||
shadow?.isVisible = showTitle
|
||||
|
||||
when (card.quality) {
|
||||
|
@ -142,15 +162,42 @@ object SearchResultBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
bg.setOnClickListener {
|
||||
click(it)
|
||||
bg.isFocusable = false
|
||||
bg.isFocusableInTouchMode = false
|
||||
if(!isTrueTvSettings()) {
|
||||
bg.setOnClickListener {
|
||||
click(it)
|
||||
}
|
||||
bg.setOnLongClickListener {
|
||||
longClick(it)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
itemView.setOnClickListener {
|
||||
click(it)
|
||||
}
|
||||
|
||||
if (nextFocusUp != null) {
|
||||
itemView.nextFocusUpId = nextFocusUp
|
||||
}
|
||||
|
||||
if (nextFocusDown != null) {
|
||||
itemView.nextFocusDownId = nextFocusDown
|
||||
}
|
||||
|
||||
/*when (nextFocusBehavior) {
|
||||
true -> itemView.nextFocusLeftId = bg.id
|
||||
false -> itemView.nextFocusRightId = bg.id
|
||||
null -> {
|
||||
bg.nextFocusRightId = -1
|
||||
bg.nextFocusLeftId = -1
|
||||
}
|
||||
}*/
|
||||
|
||||
/*if (nextFocusUp != null) {
|
||||
bg.nextFocusUpId = nextFocusUp
|
||||
}
|
||||
|
||||
|
@ -158,36 +205,26 @@ object SearchResultBuilder {
|
|||
bg.nextFocusDownId = nextFocusDown
|
||||
}
|
||||
|
||||
when (nextFocusBehavior) {
|
||||
true -> bg.nextFocusLeftId = bg.id
|
||||
false -> bg.nextFocusRightId = bg.id
|
||||
null -> {
|
||||
bg.nextFocusRightId = -1
|
||||
bg.nextFocusLeftId = -1
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
bg.isFocusable = true
|
||||
bg.isFocusableInTouchMode = true
|
||||
bg.touchscreenBlocksFocus = false
|
||||
// bg.isFocusable = true
|
||||
// bg.isFocusableInTouchMode = true
|
||||
// bg.touchscreenBlocksFocus = false
|
||||
itemView.isFocusableInTouchMode = true
|
||||
itemView.isFocusable = true
|
||||
}
|
||||
|
||||
bg.setOnLongClickListener {
|
||||
longClick(it)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
/**/
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
longClick(it)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
bg.setOnFocusChangeListener { view, b ->
|
||||
/*bg.setOnFocusChangeListener { view, b ->
|
||||
focus(view, b)
|
||||
}
|
||||
}*/
|
||||
|
||||
itemView.setOnFocusChangeListener { view, b ->
|
||||
focus(view, b)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -8,13 +8,17 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||
|
@ -22,16 +26,14 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import kotlinx.android.synthetic.main.main_settings.*
|
||||
import kotlinx.android.synthetic.main.standard_toolbar.*
|
||||
import java.io.File
|
||||
|
||||
class SettingsFragment : Fragment() {
|
||||
companion object {
|
||||
var beneneCount = 0
|
||||
|
||||
private var isTv : Boolean = false
|
||||
private var isTrueTv : Boolean = false
|
||||
private var isTv: Boolean = false
|
||||
private var isTrueTv: Boolean = false
|
||||
|
||||
fun PreferenceFragmentCompat?.getPref(id: Int): Preference? {
|
||||
if (this == null) return null
|
||||
|
@ -55,26 +57,31 @@ class SettingsFragment : Fragment() {
|
|||
|
||||
fun Fragment?.setUpToolbar(title: String) {
|
||||
if (this == null) return
|
||||
settings_toolbar?.apply {
|
||||
val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
|
||||
|
||||
settingsToolbar.apply {
|
||||
setTitle(title)
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
}
|
||||
context.fixPaddingStatusbar(settings_toolbar)
|
||||
fixPaddingStatusbar(settingsToolbar)
|
||||
}
|
||||
|
||||
fun Fragment?.setUpToolbar(@StringRes title: Int) {
|
||||
if (this == null) return
|
||||
settings_toolbar?.apply {
|
||||
val settingsToolbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) ?: return
|
||||
|
||||
settingsToolbar.apply {
|
||||
setTitle(title)
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
}
|
||||
context.fixPaddingStatusbar(settings_toolbar)
|
||||
fixPaddingStatusbar(settingsToolbar)
|
||||
}
|
||||
|
||||
fun getFolderSize(dir: File): Long {
|
||||
|
@ -139,12 +146,21 @@ class SettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
var binding: MainSettingsBinding? = null
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.main_settings, container, false)
|
||||
): View {
|
||||
val localBinding = MainSettingsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.main_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -152,41 +168,41 @@ class SettingsFragment : Fragment() {
|
|||
activity?.navigate(id, Bundle())
|
||||
}
|
||||
|
||||
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
|
||||
|
||||
val isTrueTv = isTrueTvSettings()
|
||||
|
||||
for (syncApi in accountManagers) {
|
||||
val login = syncApi.loginInfo()
|
||||
val pic = login?.profilePicture ?: continue
|
||||
if (settings_profile_pic?.setImage(
|
||||
if (binding?.settingsProfilePic?.setImage(
|
||||
pic,
|
||||
errorImageDrawable = HomeFragment.errorProfilePic
|
||||
) == true
|
||||
) {
|
||||
settings_profile_text?.text = login.name
|
||||
settings_profile?.isVisible = true
|
||||
binding?.settingsProfileText?.text = login.name
|
||||
binding?.settingsProfile?.isVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
listOf(
|
||||
Pair(settings_general, R.id.action_navigation_settings_to_navigation_settings_general),
|
||||
Pair(settings_player, R.id.action_navigation_settings_to_navigation_settings_player),
|
||||
Pair(settings_credits, R.id.action_navigation_settings_to_navigation_settings_account),
|
||||
Pair(settings_ui, R.id.action_navigation_settings_to_navigation_settings_ui),
|
||||
Pair(settings_providers, R.id.action_navigation_settings_to_navigation_settings_providers),
|
||||
Pair(settings_updates, R.id.action_navigation_settings_to_navigation_settings_updates),
|
||||
Pair(
|
||||
settings_extensions,
|
||||
R.id.action_navigation_settings_to_navigation_settings_extensions
|
||||
),
|
||||
).forEach { (view, navigationId) ->
|
||||
view?.apply {
|
||||
setOnClickListener {
|
||||
navigate(navigationId)
|
||||
}
|
||||
if (isTrueTv) {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
binding?.apply {
|
||||
listOf(
|
||||
settingsGeneral to R.id.action_navigation_settings_to_navigation_settings_general,
|
||||
settingsPlayer to R.id.action_navigation_settings_to_navigation_settings_player,
|
||||
settingsCredits to R.id.action_navigation_settings_to_navigation_settings_account,
|
||||
settingsUi to R.id.action_navigation_settings_to_navigation_settings_ui,
|
||||
settingsProviders to R.id.action_navigation_settings_to_navigation_settings_providers,
|
||||
settingsUpdates to R.id.action_navigation_settings_to_navigation_settings_updates,
|
||||
settingsExtensions to R.id.action_navigation_settings_to_navigation_settings_extensions,
|
||||
).forEach { (view, navigationId) ->
|
||||
view.apply {
|
||||
setOnClickListener {
|
||||
navigate(navigationId)
|
||||
}
|
||||
if (isTrueTv) {
|
||||
isFocusable = true
|
||||
isFocusableInTouchMode = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,8 +20,11 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding
|
||||
import com.lagradost.cloudstream3.databinding.AddSiteInputBinding
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.network.initClient
|
||||
|
@ -38,8 +41,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
|||
import com.lagradost.cloudstream3.utils.USER_PROVIDER_API
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||
import kotlinx.android.synthetic.main.add_remove_sites.*
|
||||
import kotlinx.android.synthetic.main.add_site_input.*
|
||||
import java.io.File
|
||||
|
||||
fun getCurrentLocale(context: Context): String {
|
||||
|
@ -69,6 +70,7 @@ val appLanguages = arrayListOf(
|
|||
Triple("", "español", "es"),
|
||||
Triple("", "فارسی", "fa"),
|
||||
Triple("", "français", "fr"),
|
||||
Triple("", "galego", "gl"),
|
||||
Triple("", "हिन्दी", "hi"),
|
||||
Triple("", "hrvatski", "hr"),
|
||||
Triple("", "magyar", "hu"),
|
||||
|
@ -188,7 +190,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
|
||||
|
||||
fun showAdd() {
|
||||
val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name }
|
||||
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
|
||||
activity?.showDialog(
|
||||
providers.map { "${it.name} (${it.mainUrl})" },
|
||||
-1,
|
||||
|
@ -197,21 +199,23 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
{}) { selection ->
|
||||
val provider = providers.getOrNull(selection) ?: return@showDialog
|
||||
|
||||
val binding : AddSiteInputBinding = AddSiteInputBinding.inflate(layoutInflater,null,false)
|
||||
|
||||
val builder =
|
||||
AlertDialog.Builder(context ?: return@showDialog, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.add_site_input)
|
||||
.setView(binding.root)
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
|
||||
dialog.text2?.text = provider.name
|
||||
dialog.apply_btt?.setOnClickListener {
|
||||
val name = dialog.site_name_input?.text?.toString()
|
||||
val url = dialog.site_url_input?.text?.toString()
|
||||
val lang = dialog.site_lang_input?.text?.toString()
|
||||
binding.text2.text = provider.name
|
||||
binding.applyBtt.setOnClickListener {
|
||||
val name = binding.siteNameInput.text?.toString()
|
||||
val url = binding.siteUrlInput.text?.toString()
|
||||
val lang = binding.siteLangInput.text?.toString()
|
||||
val realLang = if (lang.isNullOrBlank()) provider.lang else lang
|
||||
if (url.isNullOrBlank() || name.isNullOrBlank() || realLang.length != 2) {
|
||||
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
|
@ -219,10 +223,12 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang)
|
||||
current.add(newSite)
|
||||
setKey(USER_PROVIDER_API, current.toTypedArray())
|
||||
// reload apis
|
||||
MainActivity.afterPluginsLoadedEvent.invoke(false)
|
||||
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
dialog.cancel_btt?.setOnClickListener {
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
}
|
||||
|
@ -242,18 +248,19 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
fun showAddOrDelete() {
|
||||
val binding : AddRemoveSitesBinding = AddRemoveSitesBinding.inflate(layoutInflater,null,false)
|
||||
val builder =
|
||||
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.add_remove_sites)
|
||||
.setView(binding.root)
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
|
||||
dialog.add_site?.setOnClickListener {
|
||||
binding.addSite.setOnClickListener {
|
||||
showAdd()
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
dialog.remove_site?.setOnClickListener {
|
||||
binding.removeSite.setOnClickListener {
|
||||
showDelete()
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
|
|
|
@ -105,8 +105,10 @@ class SettingsProviders : PreferenceFragmentCompat() {
|
|||
|
||||
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
|
||||
activity?.getApiProviderLangSettings()?.let { current ->
|
||||
val languages = APIHolder.apis.map { it.lang }.toSet()
|
||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
|
||||
val languages = synchronized(APIHolder.apis) {
|
||||
APIHolder.apis.map { it.lang }.toSet()
|
||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
|
||||
}
|
||||
|
||||
val currentList = current.map {
|
||||
languages.indexOf(it)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -18,8 +18,10 @@ import androidx.navigation.fragment.findNavController
|
|||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.Some
|
||||
import com.lagradost.cloudstream3.databinding.AddRepoInputBinding
|
||||
import com.lagradost.cloudstream3.databinding.FragmentExtensionsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
|
@ -30,16 +32,22 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
|||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager
|
||||
import kotlinx.android.synthetic.main.add_repo_input.*
|
||||
import kotlinx.android.synthetic.main.fragment_extensions.*
|
||||
|
||||
class ExtensionsFragment : Fragment() {
|
||||
var binding: FragmentExtensionsBinding? = null
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_extensions, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentExtensionsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root//inflater.inflate(R.layout.fragment_extensions, container, false)
|
||||
}
|
||||
|
||||
private fun View.setLayoutWidth(weight: Int) {
|
||||
|
@ -74,7 +82,7 @@ class ExtensionsFragment : Fragment() {
|
|||
|
||||
setUpToolbar(R.string.extensions)
|
||||
|
||||
repo_recycler_view?.adapter = RepoAdapter(false, {
|
||||
binding?.repoRecyclerView?.adapter = RepoAdapter(false, {
|
||||
findNavController().navigate(
|
||||
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
|
||||
PluginsFragment.newInstance(
|
||||
|
@ -97,6 +105,7 @@ class ExtensionsFragment : Fragment() {
|
|||
extensionViewModel.loadRepositories()
|
||||
}
|
||||
}
|
||||
|
||||
DialogInterface.BUTTON_NEGATIVE -> {}
|
||||
}
|
||||
}
|
||||
|
@ -112,12 +121,12 @@ class ExtensionsFragment : Fragment() {
|
|||
})
|
||||
|
||||
observe(extensionViewModel.repositories) {
|
||||
repo_recycler_view?.isVisible = it.isNotEmpty()
|
||||
blank_repo_screen?.isVisible = it.isEmpty()
|
||||
(repo_recycler_view?.adapter as? RepoAdapter)?.updateList(it)
|
||||
binding?.repoRecyclerView?.isVisible = it.isNotEmpty()
|
||||
binding?.blankRepoScreen?.isVisible = it.isEmpty()
|
||||
(binding?.repoRecyclerView?.adapter as? RepoAdapter)?.updateList(it)
|
||||
}
|
||||
|
||||
repo_recycler_view?.apply {
|
||||
binding?.repoRecyclerView?.apply {
|
||||
context?.let { ctx ->
|
||||
layoutManager = LinearRecycleViewLayoutManager(ctx, nextFocusUpId, nextFocusDownId)
|
||||
}
|
||||
|
@ -138,32 +147,31 @@ class ExtensionsFragment : Fragment() {
|
|||
// }
|
||||
// }
|
||||
|
||||
observe(extensionViewModel.pluginStats) {
|
||||
when (it) {
|
||||
is Some.Success -> {
|
||||
val value = it.value
|
||||
observeNullable(extensionViewModel.pluginStats) { value ->
|
||||
binding?.apply {
|
||||
if (value == null) {
|
||||
pluginStorageAppbar.isVisible = false
|
||||
|
||||
plugin_storage_appbar?.isVisible = true
|
||||
if (value.total == 0) {
|
||||
plugin_download?.setLayoutWidth(1)
|
||||
plugin_disabled?.setLayoutWidth(0)
|
||||
plugin_not_downloaded?.setLayoutWidth(0)
|
||||
} else {
|
||||
plugin_download?.setLayoutWidth(value.downloaded)
|
||||
plugin_disabled?.setLayoutWidth(value.disabled)
|
||||
plugin_not_downloaded?.setLayoutWidth(value.notDownloaded)
|
||||
}
|
||||
plugin_not_downloaded_txt.setText(value.notDownloadedText)
|
||||
plugin_disabled_txt.setText(value.disabledText)
|
||||
plugin_download_txt.setText(value.downloadedText)
|
||||
return@observeNullable
|
||||
}
|
||||
is Some.None -> {
|
||||
plugin_storage_appbar?.isVisible = false
|
||||
|
||||
pluginStorageAppbar.isVisible = true
|
||||
if (value.total == 0) {
|
||||
pluginDownload.setLayoutWidth(1)
|
||||
pluginDisabled.setLayoutWidth(0)
|
||||
pluginNotDownloaded.setLayoutWidth(0)
|
||||
} else {
|
||||
pluginDownload.setLayoutWidth(value.downloaded)
|
||||
pluginDisabled.setLayoutWidth(value.disabled)
|
||||
pluginNotDownloaded.setLayoutWidth(value.notDownloaded)
|
||||
}
|
||||
pluginNotDownloadedTxt.setText(value.notDownloadedText)
|
||||
pluginDisabledTxt.setText(value.disabledText)
|
||||
pluginDownloadTxt.setText(value.downloadedText)
|
||||
}
|
||||
}
|
||||
|
||||
plugin_storage_appbar?.setOnClickListener {
|
||||
binding?.pluginStorageAppbar?.setOnClickListener {
|
||||
findNavController().navigate(
|
||||
R.id.navigation_settings_extensions_to_navigation_settings_plugins,
|
||||
PluginsFragment.newInstance(
|
||||
|
@ -175,16 +183,18 @@ class ExtensionsFragment : Fragment() {
|
|||
}
|
||||
|
||||
val addRepositoryClick = View.OnClickListener {
|
||||
val ctx = context ?: return@OnClickListener
|
||||
val binding = AddRepoInputBinding.inflate(LayoutInflater.from(ctx), null, false)
|
||||
val builder =
|
||||
AlertDialog.Builder(context ?: return@OnClickListener, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.add_repo_input)
|
||||
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
|
||||
.setView(binding.root)
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
|
||||
0
|
||||
)?.text?.toString()?.let { copy ->
|
||||
dialog.repo_url_input?.setText(copy)
|
||||
binding.repoUrlInput.setText(copy)
|
||||
}
|
||||
|
||||
// dialog.list_repositories?.setOnClickListener {
|
||||
|
@ -194,14 +204,14 @@ class ExtensionsFragment : Fragment() {
|
|||
// }
|
||||
|
||||
// dialog.text2?.text = provider.name
|
||||
dialog.apply_btt?.setOnClickListener secondListener@{
|
||||
val name = dialog.repo_name_input?.text?.toString()
|
||||
binding.applyBtt.setOnClickListener secondListener@{
|
||||
val name = binding.repoNameInput.text?.toString()
|
||||
ioSafe {
|
||||
val url = dialog.repo_url_input?.text?.toString()
|
||||
val url = binding.repoUrlInput.text?.toString()
|
||||
?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
|
||||
if (url.isNullOrBlank()) {
|
||||
main {
|
||||
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
||||
}
|
||||
} else {
|
||||
val fixedName = if (!name.isNullOrBlank()) name
|
||||
|
@ -216,22 +226,23 @@ class ExtensionsFragment : Fragment() {
|
|||
}
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
dialog.cancel_btt?.setOnClickListener {
|
||||
binding.cancelBtt.setOnClickListener {
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
}
|
||||
|
||||
val isTv = isTrueTvSettings()
|
||||
add_repo_button?.isGone = isTv
|
||||
add_repo_button_imageview_holder?.isVisible = isTv
|
||||
binding?.apply {
|
||||
addRepoButton.isGone = isTv
|
||||
addRepoButtonImageviewHolder.isVisible = isTv
|
||||
|
||||
// Band-aid for Fire TV
|
||||
plugin_storage_appbar?.isFocusableInTouchMode = isTv
|
||||
add_repo_button_imageview?.isFocusableInTouchMode = isTv
|
||||
|
||||
add_repo_button?.setOnClickListener(addRepositoryClick)
|
||||
add_repo_button_imageview?.setOnClickListener(addRepositoryClick)
|
||||
// Band-aid for Fire TV
|
||||
pluginStorageAppbar.isFocusableInTouchMode = isTv
|
||||
addRepoButtonImageview.isFocusableInTouchMode = isTv
|
||||
|
||||
addRepoButton.setOnClickListener(addRepositoryClick)
|
||||
addRepoButtonImageview.setOnClickListener(addRepositoryClick)
|
||||
}
|
||||
reloadRepositories()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.lagradost.cloudstream3.ui.settings.extensions
|
|||
import android.text.format.Formatter.formatShortFileSize
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isGone
|
||||
|
@ -13,6 +12,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity
|
||||
import com.lagradost.cloudstream3.PROVIDER_STATUS_DOWN
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
|
||||
import com.lagradost.cloudstream3.ui.result.setText
|
||||
|
@ -26,10 +26,11 @@ import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
|||
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import kotlinx.android.synthetic.main.repository_item.view.*
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.log10
|
||||
|
||||
|
||||
data class PluginViewData(
|
||||
|
@ -45,8 +46,10 @@ class PluginAdapter(
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item
|
||||
val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||
|
||||
return PluginViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||
RepositoryItemBinding.bind(inflated) // may crash
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -82,8 +85,10 @@ class PluginAdapter(
|
|||
|
||||
// Clear glide image because setImageResource doesn't override
|
||||
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
|
||||
holder.itemView.entry_icon?.let { pluginIcon ->
|
||||
GlideApp.with(pluginIcon).clear(pluginIcon)
|
||||
if (holder is PluginViewHolder) {
|
||||
holder.binding.entryIcon.let { pluginIcon ->
|
||||
GlideApp.with(pluginIcon).clear(pluginIcon)
|
||||
}
|
||||
}
|
||||
super.onViewRecycled(holder)
|
||||
}
|
||||
|
@ -112,7 +117,7 @@ class PluginAdapter(
|
|||
fun prettyCount(number: Number): String? {
|
||||
val suffix = charArrayOf(' ', 'k', 'M', 'B', 'T', 'P', 'E')
|
||||
val numValue = number.toLong()
|
||||
val value = Math.floor(Math.log10(numValue.toDouble())).toInt()
|
||||
val value = floor(log10(numValue.toDouble())).toInt()
|
||||
val base = value / 3
|
||||
return if (value >= 3 && base < suffix.size) {
|
||||
DecimalFormat("#0.00").format(
|
||||
|
@ -127,8 +132,8 @@ class PluginAdapter(
|
|||
}
|
||||
}
|
||||
|
||||
inner class PluginViewHolder(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
inner class PluginViewHolder(val binding: RepositoryItemBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(
|
||||
data: PluginViewData,
|
||||
|
@ -138,17 +143,17 @@ class PluginAdapter(
|
|||
val name = metadata.name.removeSuffix("Provider")
|
||||
val alpha = if (disabled) 0.6f else 1f
|
||||
val isLocal = !data.plugin.second.url.startsWith("http")
|
||||
itemView.main_text?.alpha = alpha
|
||||
itemView.sub_text?.alpha = alpha
|
||||
binding.mainText.alpha = alpha
|
||||
binding.subText.alpha = alpha
|
||||
|
||||
val drawableInt = if (data.isDownloaded)
|
||||
R.drawable.ic_baseline_delete_outline_24
|
||||
else R.drawable.netflix_download
|
||||
|
||||
itemView.nsfw_marker?.isVisible = metadata.tvTypes?.contains("NSFW") ?: false
|
||||
itemView.action_button?.setImageResource(drawableInt)
|
||||
binding.nsfwMarker.isVisible = metadata.tvTypes?.contains("NSFW") ?: false
|
||||
binding.actionButton.setImageResource(drawableInt)
|
||||
|
||||
itemView.action_button?.setOnClickListener {
|
||||
binding.actionButton.setOnClickListener {
|
||||
iconClickCallback.invoke(data.plugin)
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
|
@ -169,10 +174,11 @@ class PluginAdapter(
|
|||
|
||||
if (data.isDownloaded) {
|
||||
// On local plugins page the filepath is provided instead of url.
|
||||
val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
|
||||
val plugin =
|
||||
PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
|
||||
if (plugin?.openSettings != null) {
|
||||
itemView.action_settings?.isVisible = true
|
||||
itemView.action_settings.setOnClickListener {
|
||||
binding.actionSettings.isVisible = true
|
||||
binding.actionSettings.setOnClickListener {
|
||||
try {
|
||||
plugin.openSettings!!.invoke(itemView.context)
|
||||
} catch (e: Throwable) {
|
||||
|
@ -185,13 +191,13 @@ class PluginAdapter(
|
|||
}
|
||||
}
|
||||
} else {
|
||||
itemView.action_settings?.isVisible = false
|
||||
binding.actionSettings.isVisible = false
|
||||
}
|
||||
} else {
|
||||
itemView.action_settings?.isVisible = false
|
||||
binding.actionSettings.isVisible = false
|
||||
}
|
||||
|
||||
if (itemView.entry_icon?.setImage(//itemView.entry_icon?.height ?:
|
||||
if (!binding.entryIcon.setImage(//itemView.entry_icon?.height ?:
|
||||
metadata.iconUrl?.replace(
|
||||
"%size%",
|
||||
"$iconSize"
|
||||
|
@ -201,41 +207,47 @@ class PluginAdapter(
|
|||
),
|
||||
null,
|
||||
errorImageDrawable = R.drawable.ic_baseline_extension_24
|
||||
) != true
|
||||
)
|
||||
) {
|
||||
itemView.entry_icon?.setImageResource(R.drawable.ic_baseline_extension_24)
|
||||
binding.entryIcon.setImageResource(R.drawable.ic_baseline_extension_24)
|
||||
}
|
||||
|
||||
itemView.ext_version?.isVisible = true
|
||||
itemView.ext_version?.text = "v${metadata.version}"
|
||||
binding.extVersion.isVisible = true
|
||||
binding.extVersion.text = "v${metadata.version}"
|
||||
|
||||
if (metadata.language.isNullOrBlank()) {
|
||||
itemView.lang_icon?.isVisible = false
|
||||
binding.langIcon.isVisible = false
|
||||
} else {
|
||||
itemView.lang_icon?.isVisible = true
|
||||
itemView.lang_icon.text = "${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
|
||||
binding.langIcon.isVisible = true
|
||||
binding.langIcon.text =
|
||||
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
|
||||
}
|
||||
|
||||
itemView.ext_votes?.isVisible = false
|
||||
binding.extVotes.isVisible = false
|
||||
if (!isLocal) {
|
||||
ioSafe {
|
||||
metadata.getVotes().main {
|
||||
itemView.ext_votes?.setText(txt(R.string.extension_rating, prettyCount(it)))
|
||||
itemView.ext_votes?.isVisible = true
|
||||
binding.extVotes.setText(txt(R.string.extension_rating, prettyCount(it)))
|
||||
binding.extVotes.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (metadata.fileSize != null) {
|
||||
itemView.ext_filesize?.isVisible = true
|
||||
itemView.ext_filesize?.text = formatShortFileSize(itemView.context, metadata.fileSize)
|
||||
binding.extFilesize.isVisible = true
|
||||
binding.extFilesize.text = formatShortFileSize(itemView.context, metadata.fileSize)
|
||||
} else {
|
||||
itemView.ext_filesize?.isVisible = false
|
||||
binding.extFilesize.isVisible = false
|
||||
}
|
||||
itemView.main_text.setText(if(disabled) txt(R.string.single_plugin_disabled, name) else txt(name))
|
||||
itemView.sub_text?.isGone = metadata.description.isNullOrBlank()
|
||||
itemView.sub_text?.text = metadata.description.html()
|
||||
binding.mainText.setText(
|
||||
if (disabled) txt(
|
||||
R.string.single_plugin_disabled,
|
||||
name
|
||||
) else txt(name)
|
||||
)
|
||||
binding.subText.isGone = metadata.description.isNullOrBlank()
|
||||
binding.subText.text = metadata.description.html()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,30 +2,29 @@ package com.lagradost.cloudstream3.ui.settings.extensions
|
|||
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import android.text.format.Formatter.formatFileSize
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import kotlinx.android.synthetic.main.fragment_plugin_details.*
|
||||
import android.text.format.Formatter.formatFileSize
|
||||
import android.util.Log
|
||||
import androidx.core.view.isVisible
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPluginDetailsBinding
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.VotingApi
|
||||
import com.lagradost.cloudstream3.plugins.VotingApi.canVote
|
||||
import com.lagradost.cloudstream3.plugins.VotingApi.getVoteType
|
||||
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
|
||||
import com.lagradost.cloudstream3.plugins.VotingApi.vote
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.plugins.VotingApi.canVote
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso
|
||||
import kotlinx.android.synthetic.main.repository_item.view.*
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
|
||||
|
||||
class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragment() {
|
||||
|
@ -43,18 +42,27 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
var binding: FragmentPluginDetailsBinding? = null
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_plugin_details, container, false)
|
||||
|
||||
): View {
|
||||
val localBinding = FragmentPluginDetailsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.fragment_plugin_details, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val metadata = data.plugin.second
|
||||
if (plugin_icon?.setImage(//plugin_icon?.height ?:
|
||||
binding?.apply {
|
||||
if (!pluginIcon.setImage(//plugin_icon?.height ?:
|
||||
metadata.iconUrl?.replace(
|
||||
"%size%",
|
||||
"$iconSize"
|
||||
|
@ -64,23 +72,33 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
|
|||
),
|
||||
null,
|
||||
errorImageDrawable = R.drawable.ic_baseline_extension_24
|
||||
) != true
|
||||
)
|
||||
) {
|
||||
plugin_icon?.setImageResource(R.drawable.ic_baseline_extension_24)
|
||||
pluginIcon.setImageResource(R.drawable.ic_baseline_extension_24)
|
||||
}
|
||||
plugin_name?.text = metadata.name.removeSuffix("Provider")
|
||||
plugin_version?.text = metadata.version.toString()
|
||||
plugin_description?.text = metadata.description ?: getString(R.string.no_data)
|
||||
plugin_size?.text = if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(context, metadata.fileSize)
|
||||
plugin_author?.text = if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(", ")
|
||||
plugin_status?.text = resources.getStringArray(R.array.extension_statuses)[metadata.status]
|
||||
plugin_types?.text = if ((metadata.tvTypes == null) || metadata.tvTypes.isEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(", ")
|
||||
plugin_lang?.text = if (metadata.language == null)
|
||||
getString(R.string.no_data)
|
||||
pluginName.text = metadata.name.removeSuffix("Provider")
|
||||
pluginVersion.text = metadata.version.toString()
|
||||
pluginDescription.text = metadata.description ?: getString(R.string.no_data)
|
||||
pluginSize.text =
|
||||
if (metadata.fileSize == null) getString(R.string.no_data) else formatFileSize(
|
||||
context,
|
||||
metadata.fileSize
|
||||
)
|
||||
pluginAuthor.text =
|
||||
if (metadata.authors.isEmpty()) getString(R.string.no_data) else metadata.authors.joinToString(
|
||||
", "
|
||||
)
|
||||
pluginStatus.text = resources.getStringArray(R.array.extension_statuses)[metadata.status]
|
||||
pluginTypes.text =
|
||||
if (metadata.tvTypes.isNullOrEmpty()) getString(R.string.no_data) else metadata.tvTypes.joinToString(
|
||||
", "
|
||||
)
|
||||
pluginLang.text = if (metadata.language == null)
|
||||
getString(R.string.no_data)
|
||||
else
|
||||
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
|
||||
"${getFlagFromIso(metadata.language)} ${fromTwoLettersToLanguage(metadata.language)}"
|
||||
|
||||
github_btn.setOnClickListener {
|
||||
githubBtn.setOnClickListener {
|
||||
if (metadata.repositoryUrl != null) {
|
||||
openBrowser(metadata.repositoryUrl)
|
||||
}
|
||||
|
@ -93,10 +111,11 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
|
|||
|
||||
if (data.isDownloaded) {
|
||||
// On local plugins page the filepath is provided instead of url.
|
||||
val plugin = PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
|
||||
val plugin =
|
||||
PluginManager.urlPlugins[metadata.url] ?: PluginManager.plugins[metadata.url]
|
||||
if (plugin?.openSettings != null && context != null) {
|
||||
action_settings?.isVisible = true
|
||||
action_settings.setOnClickListener {
|
||||
actionSettings.isVisible = true
|
||||
actionSettings.setOnClickListener {
|
||||
try {
|
||||
plugin.openSettings!!.invoke(requireContext())
|
||||
} catch (e: Throwable) {
|
||||
|
@ -109,10 +128,10 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
|
|||
}
|
||||
}
|
||||
} else {
|
||||
action_settings?.isVisible = false
|
||||
actionSettings.isVisible = false
|
||||
}
|
||||
} else {
|
||||
action_settings?.isVisible = false
|
||||
actionSettings.isVisible = false
|
||||
}
|
||||
|
||||
upvote.setOnClickListener {
|
||||
|
@ -136,23 +155,40 @@ class PluginDetailsFragment(val data: PluginViewData) : BottomSheetDialogFragmen
|
|||
updateVoting(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVoting(value: Int) {
|
||||
val metadata = data.plugin.second
|
||||
plugin_votes.text = value.toString()
|
||||
when (metadata.getVoteType()) {
|
||||
VotingApi.VoteType.UPVOTE -> {
|
||||
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary)
|
||||
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
|
||||
}
|
||||
VotingApi.VoteType.DOWNVOTE -> {
|
||||
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary)
|
||||
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
|
||||
}
|
||||
VotingApi.VoteType.NONE -> {
|
||||
upvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
|
||||
downvote.imageTintList = ColorStateList.valueOf(context?.colorFromAttribute(R.attr.white) ?: R.color.white)
|
||||
binding?.apply {
|
||||
pluginVotes.text = value.toString()
|
||||
when (metadata.getVoteType()) {
|
||||
VotingApi.VoteType.UPVOTE -> {
|
||||
upvote.imageTintList = ColorStateList.valueOf(
|
||||
context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary
|
||||
)
|
||||
downvote.imageTintList = ColorStateList.valueOf(
|
||||
context?.colorFromAttribute(R.attr.white) ?: R.color.white
|
||||
)
|
||||
}
|
||||
|
||||
VotingApi.VoteType.DOWNVOTE -> {
|
||||
downvote.imageTintList = ColorStateList.valueOf(
|
||||
context?.colorFromAttribute(R.attr.colorPrimary) ?: R.color.colorPrimary
|
||||
)
|
||||
upvote.imageTintList = ColorStateList.valueOf(
|
||||
context?.colorFromAttribute(R.attr.white) ?: R.color.white
|
||||
)
|
||||
}
|
||||
|
||||
VotingApi.VoteType.NONE -> {
|
||||
upvote.imageTintList = ColorStateList.valueOf(
|
||||
context?.colorFromAttribute(R.attr.white) ?: R.color.white
|
||||
)
|
||||
downvote.imageTintList = ColorStateList.valueOf(
|
||||
context?.colorFromAttribute(R.attr.white) ?: R.color.white
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
|||
import com.lagradost.cloudstream3.AllLanguagesName
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.databinding.FragmentPluginsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
|
@ -20,9 +21,6 @@ import com.lagradost.cloudstream3.ui.settings.appLanguages
|
|||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||
import kotlinx.android.synthetic.main.fragment_plugins.*
|
||||
import kotlinx.android.synthetic.main.tvtypes_chips.*
|
||||
import kotlinx.android.synthetic.main.tvtypes_chips_scroll.*
|
||||
|
||||
const val PLUGINS_BUNDLE_NAME = "name"
|
||||
const val PLUGINS_BUNDLE_URL = "url"
|
||||
|
@ -33,11 +31,19 @@ class PluginsFragment : Fragment() {
|
|||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_plugins, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentPluginsBinding.inflate(inflater,container,false)
|
||||
binding = localBinding
|
||||
return localBinding.root//inflater.inflate(R.layout.fragment_plugins, container, false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private val pluginViewModel: PluginsViewModel by activityViewModels()
|
||||
var binding: FragmentPluginsBinding? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
@ -66,8 +72,8 @@ class PluginsFragment : Fragment() {
|
|||
}
|
||||
|
||||
setUpToolbar(name)
|
||||
|
||||
settings_toolbar?.setOnMenuItemClickListener { menuItem ->
|
||||
binding?.settingsToolbar?.apply {
|
||||
setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem?.itemId) {
|
||||
R.id.download_all -> {
|
||||
PluginsViewModel.downloadAll(activity, url, pluginViewModel)
|
||||
|
@ -99,67 +105,69 @@ class PluginsFragment : Fragment() {
|
|||
}
|
||||
|
||||
val searchView =
|
||||
settings_toolbar?.menu?.findItem(R.id.search_button)?.actionView as? SearchView
|
||||
menu?.findItem(R.id.search_button)?.actionView as? SearchView
|
||||
|
||||
// Don't go back if active query
|
||||
settings_toolbar?.setNavigationOnClickListener {
|
||||
setNavigationOnClickListener {
|
||||
if (searchView?.isIconified == false) {
|
||||
searchView.isIconified = true
|
||||
} else {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
}
|
||||
searchView?.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) pluginViewModel.search(null)
|
||||
}
|
||||
|
||||
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
pluginViewModel.search(query)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
pluginViewModel.search(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
// searchView?.onActionViewCollapsed = {
|
||||
// pluginViewModel.search(null)
|
||||
// }
|
||||
|
||||
// Because onActionViewCollapsed doesn't wanna work we need this workaround :(
|
||||
searchView?.setOnQueryTextFocusChangeListener { _, hasFocus ->
|
||||
if (!hasFocus) pluginViewModel.search(null)
|
||||
}
|
||||
|
||||
searchView?.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
pluginViewModel.search(query)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
pluginViewModel.search(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
plugin_recycler_view?.adapter =
|
||||
|
||||
binding?.pluginRecyclerView?.adapter =
|
||||
PluginAdapter {
|
||||
pluginViewModel.handlePluginAction(activity, url, it, isLocal)
|
||||
}
|
||||
|
||||
if (isTvSettings()) {
|
||||
// Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that.
|
||||
plugin_recycler_view?.setPadding(0, 0, 0, 200.toPx)
|
||||
binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx)
|
||||
}
|
||||
|
||||
observe(pluginViewModel.filteredPlugins) { (scrollToTop, list) ->
|
||||
(plugin_recycler_view?.adapter as? PluginAdapter)?.updateList(list)
|
||||
(binding?.pluginRecyclerView?.adapter as? PluginAdapter)?.updateList(list)
|
||||
|
||||
if (scrollToTop)
|
||||
plugin_recycler_view?.scrollToPosition(0)
|
||||
binding?.pluginRecyclerView?.scrollToPosition(0)
|
||||
}
|
||||
|
||||
if (isLocal) {
|
||||
// No download button and no categories on local
|
||||
settings_toolbar?.menu?.findItem(R.id.download_all)?.isVisible = false
|
||||
settings_toolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false
|
||||
binding?.settingsToolbar?.menu?.findItem(R.id.download_all)?.isVisible = false
|
||||
binding?.settingsToolbar?.menu?.findItem(R.id.lang_filter)?.isVisible = false
|
||||
pluginViewModel.updatePluginListLocal()
|
||||
tv_types_scroll_view?.isVisible = false
|
||||
|
||||
binding?.tvtypesChipsScroll?.root?.isVisible = false
|
||||
} else {
|
||||
pluginViewModel.updatePluginList(context, url)
|
||||
tv_types_scroll_view?.isVisible = true
|
||||
binding?.tvtypesChipsScroll?.root?.isVisible = true
|
||||
|
||||
bindChips(home_select_group, emptyList(), TvType.values().toList()) { list ->
|
||||
bindChips(binding?.tvtypesChipsScroll?.tvtypesChips, emptyList(), TvType.values().toList()) { list ->
|
||||
pluginViewModel.tvTypes.clear()
|
||||
pluginViewModel.tvTypes.addAll(list.map { it.name })
|
||||
pluginViewModel.updateFilteredPlugins()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.extensions
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
|
||||
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import kotlinx.android.synthetic.main.repository_item.view.*
|
||||
|
||||
class RepoAdapter(
|
||||
val isSetup: Boolean,
|
||||
|
@ -20,9 +21,17 @@ class RepoAdapter(
|
|||
private val repositories: MutableList<RepositoryData> = mutableListOf()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item
|
||||
val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
) else RepositoryItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
) //R.layout.repository_item_tv else R.layout.repository_item
|
||||
return RepoViewHolder(
|
||||
LayoutInflater.from(parent.context).inflate(layout, parent, false)
|
||||
layout
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -57,30 +66,57 @@ class RepoAdapter(
|
|||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
inner class RepoViewHolder(itemView: View) :
|
||||
RecyclerView.ViewHolder(itemView) {
|
||||
inner class RepoViewHolder(
|
||||
val binding: ViewBinding
|
||||
) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(
|
||||
repositoryData: RepositoryData
|
||||
) {
|
||||
val isPrebuilt = PREBUILT_REPOSITORIES.contains(repositoryData)
|
||||
val drawable =
|
||||
if (isSetup) R.drawable.netflix_download else R.drawable.ic_baseline_delete_outline_24
|
||||
when (binding) {
|
||||
is RepositoryItemTvBinding -> {
|
||||
binding.apply {
|
||||
// Only shows icon if on setup or if it isn't a prebuilt repo.
|
||||
// No delete buttons on prebuilt repos.
|
||||
if (!isPrebuilt || isSetup) {
|
||||
actionButton.setImageResource(drawable)
|
||||
}
|
||||
|
||||
// Only shows icon if on setup or if it isn't a prebuilt repo.
|
||||
// No delete buttons on prebuilt repos.
|
||||
if (!isPrebuilt || isSetup) {
|
||||
itemView.action_button?.setImageResource(drawable)
|
||||
}
|
||||
actionButton.setOnClickListener {
|
||||
imageClickCallback(repositoryData)
|
||||
}
|
||||
|
||||
itemView.action_button?.setOnClickListener {
|
||||
imageClickCallback(repositoryData)
|
||||
}
|
||||
repositoryItemRoot.setOnClickListener {
|
||||
clickCallback(repositoryData)
|
||||
}
|
||||
mainText.text = repositoryData.name
|
||||
subText.text = repositoryData.url
|
||||
}
|
||||
}
|
||||
|
||||
itemView.repository_item_root?.setOnClickListener {
|
||||
clickCallback(repositoryData)
|
||||
is RepositoryItemBinding -> {
|
||||
binding.apply {
|
||||
// Only shows icon if on setup or if it isn't a prebuilt repo.
|
||||
// No delete buttons on prebuilt repos.
|
||||
if (!isPrebuilt || isSetup) {
|
||||
actionButton.setImageResource(drawable)
|
||||
}
|
||||
|
||||
actionButton.setOnClickListener {
|
||||
imageClickCallback(repositoryData)
|
||||
}
|
||||
|
||||
repositoryItemRoot.setOnClickListener {
|
||||
clickCallback(repositoryData)
|
||||
}
|
||||
mainText.text = repositoryData.name
|
||||
subText.text = repositoryData.url
|
||||
}
|
||||
}
|
||||
}
|
||||
itemView.main_text?.text = repositoryData.name
|
||||
itemView.sub_text?.text = repositoryData.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,97 +1,105 @@
|
|||
package com.lagradost.cloudstream3.ui.settings.testing
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentTestingBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.mvvm.observe
|
||||
import com.lagradost.cloudstream3.mvvm.observeNullable
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||
import kotlinx.android.synthetic.main.fragment_testing.*
|
||||
import kotlinx.android.synthetic.main.view_test.*
|
||||
|
||||
|
||||
class TestFragment : Fragment() {
|
||||
|
||||
private val testViewModel: TestViewModel by activityViewModels()
|
||||
var binding: FragmentTestingBinding? = null
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
setUpToolbar(R.string.category_provider_test)
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
provider_test_recycler_view?.adapter = TestResultAdapter(
|
||||
mutableListOf()
|
||||
)
|
||||
binding?.apply {
|
||||
providerTestRecyclerView.adapter = TestResultAdapter(
|
||||
mutableListOf()
|
||||
)
|
||||
|
||||
testViewModel.init()
|
||||
if (testViewModel.isRunningTest) {
|
||||
provider_test?.setState(TestView.TestState.Running)
|
||||
}
|
||||
|
||||
observe(testViewModel.providerProgress) { (passed, failed, total) ->
|
||||
provider_test?.setProgress(passed, failed, total)
|
||||
}
|
||||
|
||||
observeNullable(testViewModel.providerResults) {
|
||||
normalSafeApiCall {
|
||||
val newItems = it.sortedBy { api -> api.first.name }
|
||||
(provider_test_recycler_view?.adapter as? TestResultAdapter)?.updateList(
|
||||
newItems
|
||||
)
|
||||
testViewModel.init()
|
||||
if (testViewModel.isRunningTest) {
|
||||
providerTest.setState(TestView.TestState.Running)
|
||||
}
|
||||
}
|
||||
|
||||
provider_test?.setOnPlayButtonListener { state ->
|
||||
when (state) {
|
||||
TestView.TestState.Stopped -> testViewModel.stopTest()
|
||||
TestView.TestState.Running -> testViewModel.startTest()
|
||||
TestView.TestState.None -> testViewModel.startTest()
|
||||
observe(testViewModel.providerProgress) { (passed, failed, total) ->
|
||||
providerTest.setProgress(passed, failed, total)
|
||||
}
|
||||
}
|
||||
|
||||
if (isTrueTvSettings()) {
|
||||
tests_play_pause?.isFocusableInTouchMode = true
|
||||
tests_play_pause?.requestFocus()
|
||||
}
|
||||
|
||||
provider_test?.playPauseButton?.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
provider_test_appbar?.setExpanded(true, true)
|
||||
observeNullable(testViewModel.providerResults) {
|
||||
normalSafeApiCall {
|
||||
val newItems = it.sortedBy { api -> api.first.name }
|
||||
(providerTestRecyclerView.adapter as? TestResultAdapter)?.updateList(
|
||||
newItems
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
providerTest.setOnPlayButtonListener { state ->
|
||||
when (state) {
|
||||
TestView.TestState.Stopped -> testViewModel.stopTest()
|
||||
TestView.TestState.Running -> testViewModel.startTest()
|
||||
TestView.TestState.None -> testViewModel.startTest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun focusRecyclerView() {
|
||||
// Hack to make it possible to focus the recyclerview.
|
||||
if (isTrueTvSettings()) {
|
||||
provider_test_recycler_view?.requestFocus()
|
||||
provider_test_appbar?.setExpanded(false, true)
|
||||
providerTest.playPauseButton?.isFocusableInTouchMode = true
|
||||
providerTest.playPauseButton?.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
provider_test?.setOnMainClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All)
|
||||
focusRecyclerView()
|
||||
}
|
||||
provider_test?.setOnFailedClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed)
|
||||
focusRecyclerView()
|
||||
}
|
||||
provider_test?.setOnPassedClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed)
|
||||
focusRecyclerView()
|
||||
providerTest.playPauseButton?.setOnFocusChangeListener { _, hasFocus ->
|
||||
if (hasFocus) {
|
||||
providerTestAppbar.setExpanded(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun focusRecyclerView() {
|
||||
// Hack to make it possible to focus the recyclerview.
|
||||
if (isTrueTvSettings()) {
|
||||
providerTestRecyclerView.requestFocus()
|
||||
providerTestAppbar.setExpanded(false, true)
|
||||
}
|
||||
}
|
||||
|
||||
providerTest.setOnMainClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.All)
|
||||
focusRecyclerView()
|
||||
}
|
||||
providerTest.setOnFailedClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Failed)
|
||||
focusRecyclerView()
|
||||
}
|
||||
providerTest.setOnPassedClick {
|
||||
testViewModel.setFilterMethod(TestViewModel.ProviderFilter.Passed)
|
||||
focusRecyclerView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_testing, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentTestingBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root//inflater.inflate(R.layout.fragment_testing, container, false)
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -8,21 +8,16 @@ import androidx.core.view.isVisible
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||
import com.lagradost.cloudstream3.AllLanguagesName
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSetupExtensionsBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel
|
||||
import com.lagradost.cloudstream3.ui.settings.extensions.RepoAdapter
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.main
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import kotlinx.android.synthetic.main.fragment_extensions.blank_repo_screen
|
||||
import kotlinx.android.synthetic.main.fragment_extensions.repo_recycler_view
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.setup_root
|
||||
|
||||
|
||||
class SetupFragmentExtensions : Fragment() {
|
||||
|
@ -39,13 +34,24 @@ class SetupFragmentExtensions : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
var binding: FragmentSetupExtensionsBinding? = null
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_setup_extensions, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentSetupExtensionsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.fragment_setup_extensions, container, false)
|
||||
}
|
||||
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
afterRepositoryLoadedEvent += ::setRepositories
|
||||
|
@ -60,12 +66,12 @@ class SetupFragmentExtensions : Fragment() {
|
|||
main {
|
||||
val repositories = RepositoryManager.getRepositories() + PREBUILT_REPOSITORIES
|
||||
val hasRepos = repositories.isNotEmpty()
|
||||
repo_recycler_view?.isVisible = hasRepos
|
||||
blank_repo_screen?.isVisible = !hasRepos
|
||||
binding?.repoRecyclerView?.isVisible = hasRepos
|
||||
binding?.blankRepoScreen?.isVisible = !hasRepos
|
||||
// view_public_repositories_button?.isVisible = hasRepos
|
||||
|
||||
if (hasRepos) {
|
||||
repo_recycler_view?.adapter = RepoAdapter(true, {}, {
|
||||
binding?.repoRecyclerView?.adapter = RepoAdapter(true, {}, {
|
||||
PluginsViewModel.downloadAll(activity, it.url, null)
|
||||
}).apply { updateList(repositories) }
|
||||
}
|
||||
|
@ -80,39 +86,40 @@ class SetupFragmentExtensions : Fragment() {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(setup_root)
|
||||
fixPaddingStatusbar(binding?.setupRoot)
|
||||
val isSetup = arguments?.getBoolean(SETUP_EXTENSION_BUNDLE_IS_SETUP) ?: false
|
||||
|
||||
// view_public_repositories_button?.setOnClickListener {
|
||||
// openBrowser(PUBLIC_REPOSITORIES_LIST, isTvSettings(), this)
|
||||
// }
|
||||
|
||||
with(context) {
|
||||
if (this == null) return
|
||||
normalSafeApiCall {
|
||||
// val ctx = context ?: return@normalSafeApiCall
|
||||
setRepositories()
|
||||
binding?.apply {
|
||||
if (!isSetup) {
|
||||
nextBtt.setText(R.string.setup_done)
|
||||
}
|
||||
prevBtt.isVisible = isSetup
|
||||
|
||||
if (!isSetup) {
|
||||
next_btt.setText(R.string.setup_done)
|
||||
}
|
||||
prev_btt?.isVisible = isSetup
|
||||
nextBtt.setOnClickListener {
|
||||
// Continue setup
|
||||
if (isSetup)
|
||||
if (
|
||||
// If any available languages
|
||||
synchronized(apis) { apis.distinctBy { it.lang }.size > 1 }
|
||||
) {
|
||||
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
|
||||
}
|
||||
else
|
||||
findNavController().navigate(R.id.navigation_home)
|
||||
}
|
||||
|
||||
next_btt?.setOnClickListener {
|
||||
// Continue setup
|
||||
if (isSetup)
|
||||
if (
|
||||
// If any available languages
|
||||
apis.distinctBy { it.lang }.size > 1
|
||||
) {
|
||||
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
|
||||
}
|
||||
else
|
||||
findNavController().navigate(R.id.navigation_home)
|
||||
}
|
||||
|
||||
prev_btt?.setOnClickListener {
|
||||
findNavController().navigate(R.id.navigation_setup_language)
|
||||
prevBtt.setOnClickListener {
|
||||
findNavController().navigate(R.id.navigation_setup_language)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,40 +13,49 @@ import androidx.preference.PreferenceManager
|
|||
import com.lagradost.cloudstream3.BuildConfig
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSetupLanguageBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.settings.appLanguages
|
||||
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
|
||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import kotlinx.android.synthetic.main.fragment_setup_language.*
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
||||
|
||||
const val HAS_DONE_SETUP_KEY = "HAS_DONE_SETUP"
|
||||
|
||||
class SetupFragmentLanguage : Fragment() {
|
||||
var binding: FragmentSetupLanguageBinding? = null
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_setup_language, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentSetupLanguageBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.fragment_setup_language, container, false)
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(setup_root)
|
||||
|
||||
// We don't want a crash for all users
|
||||
normalSafeApiCall {
|
||||
with(context) {
|
||||
if (this == null) return@normalSafeApiCall
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
fixPaddingStatusbar(binding?.setupRoot)
|
||||
|
||||
val arrayAdapter =
|
||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
||||
val ctx = context ?: return@normalSafeApiCall
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
val arrayAdapter =
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
binding?.apply {
|
||||
// Icons may crash on some weird android versions?
|
||||
normalSafeApiCall {
|
||||
val drawable = when {
|
||||
|
@ -54,10 +63,10 @@ class SetupFragmentLanguage : Fragment() {
|
|||
BuildConfig.BUILD_TYPE == "prerelease" -> R.drawable.cloud_2_gradient_beta
|
||||
else -> R.drawable.cloud_2_gradient
|
||||
}
|
||||
app_icon_image?.setImageDrawable(ContextCompat.getDrawable(this, drawable))
|
||||
appIconImage.setImageDrawable(ContextCompat.getDrawable(ctx, drawable))
|
||||
}
|
||||
|
||||
val current = getCurrentLocale(this)
|
||||
val current = getCurrentLocale(ctx)
|
||||
val languageCodes = appLanguages.map { it.third }
|
||||
val languageNames = appLanguages.map { (emoji, name, iso) ->
|
||||
val flag = emoji.ifBlank { SubtitleHelper.getFlagFromIso(iso) ?: "ERROR" }
|
||||
|
@ -66,18 +75,19 @@ class SetupFragmentLanguage : Fragment() {
|
|||
val index = languageCodes.indexOf(current)
|
||||
|
||||
arrayAdapter.addAll(languageNames)
|
||||
listview1?.adapter = arrayAdapter
|
||||
listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listview1?.setItemChecked(index, true)
|
||||
listview1.adapter = arrayAdapter
|
||||
listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listview1.setItemChecked(index, true)
|
||||
|
||||
listview1?.setOnItemClickListener { _, _, position, _ ->
|
||||
listview1.setOnItemClickListener { _, _, position, _ ->
|
||||
val code = languageCodes[position]
|
||||
CommonActivity.setLocale(activity, code)
|
||||
settingsManager.edit().putString(getString(R.string.locale_key), code).apply()
|
||||
settingsManager.edit().putString(getString(R.string.locale_key), code)
|
||||
.apply()
|
||||
activity?.recreate()
|
||||
}
|
||||
|
||||
next_btt?.setOnClickListener {
|
||||
nextBtt.setOnClickListener {
|
||||
// If no plugins go to plugins page
|
||||
val nextDestination = if (
|
||||
PluginManager.getPluginsOnline().isEmpty()
|
||||
|
@ -92,10 +102,11 @@ class SetupFragmentLanguage : Fragment() {
|
|||
)
|
||||
}
|
||||
|
||||
skip_btt?.setOnClickListener {
|
||||
skipBtt.setOnClickListener {
|
||||
findNavController().navigate(R.id.navigation_home)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,30 +10,39 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import kotlinx.android.synthetic.main.fragment_setup_layout.*
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.listview1
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.next_btt
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.prev_btt
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.setup_root
|
||||
import org.acra.ACRA
|
||||
|
||||
|
||||
class SetupFragmentLayout : Fragment() {
|
||||
|
||||
var binding: FragmentSetupLayoutBinding? = null
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_setup_layout, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentSetupLayoutBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.fragment_setup_layout, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(setup_root)
|
||||
fixPaddingStatusbar(binding?.setupRoot)
|
||||
|
||||
with(context) {
|
||||
if (this == null) return
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
normalSafeApiCall {
|
||||
val ctx = context ?: return@normalSafeApiCall
|
||||
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
val prefNames = resources.getStringArray(R.array.app_layout)
|
||||
val prefValues = resources.getIntArray(R.array.app_layout_values)
|
||||
|
@ -42,48 +51,48 @@ class SetupFragmentLayout : Fragment() {
|
|||
settingsManager.getInt(getString(R.string.app_layout_key), -1)
|
||||
|
||||
val arrayAdapter =
|
||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
arrayAdapter.addAll(prefNames.toList())
|
||||
listview1?.adapter = arrayAdapter
|
||||
listview1?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listview1?.setItemChecked(
|
||||
prefValues.indexOf(currentLayout), true
|
||||
)
|
||||
|
||||
listview1?.setOnItemClickListener { _, _, position, _ ->
|
||||
settingsManager.edit()
|
||||
.putInt(getString(R.string.app_layout_key), prefValues[position])
|
||||
.apply()
|
||||
activity?.recreate()
|
||||
}
|
||||
|
||||
acra_switch?.setOnCheckedChangeListener { _, enableCrashReporting ->
|
||||
// Use same pref as in settings
|
||||
settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting)
|
||||
.apply()
|
||||
val text =
|
||||
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
|
||||
crash_reporting_text?.text = getText(text)
|
||||
}
|
||||
|
||||
val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true)
|
||||
acra_switch.isChecked = enableCrashReporting
|
||||
crash_reporting_text.text =
|
||||
getText(
|
||||
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
|
||||
binding?.apply {
|
||||
listview1.adapter = arrayAdapter
|
||||
listview1.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listview1.setItemChecked(
|
||||
prefValues.indexOf(currentLayout), true
|
||||
)
|
||||
|
||||
listview1.setOnItemClickListener { _, _, position, _ ->
|
||||
settingsManager.edit()
|
||||
.putInt(getString(R.string.app_layout_key), prefValues[position])
|
||||
.apply()
|
||||
activity?.recreate()
|
||||
}
|
||||
acraSwitch.setOnCheckedChangeListener { _, enableCrashReporting ->
|
||||
// Use same pref as in settings
|
||||
settingsManager.edit().putBoolean(ACRA.PREF_DISABLE_ACRA, !enableCrashReporting)
|
||||
.apply()
|
||||
val text =
|
||||
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
|
||||
crashReportingText.text = getText(text)
|
||||
}
|
||||
|
||||
next_btt?.setOnClickListener {
|
||||
findNavController().navigate(R.id.navigation_home)
|
||||
}
|
||||
val enableCrashReporting = !settingsManager.getBoolean(ACRA.PREF_DISABLE_ACRA, true)
|
||||
|
||||
prev_btt?.setOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
acraSwitch.isChecked = enableCrashReporting
|
||||
crashReportingText.text =
|
||||
getText(
|
||||
if (enableCrashReporting) R.string.bug_report_settings_off else R.string.bug_report_settings_on
|
||||
)
|
||||
|
||||
|
||||
nextBtt.setOnClickListener {
|
||||
findNavController().navigate(R.id.navigation_home)
|
||||
}
|
||||
|
||||
prevBtt.setOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -10,72 +10,85 @@ import androidx.core.util.forEach
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.TvType
|
||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||
import kotlinx.android.synthetic.main.fragment_setup_media.*
|
||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
||||
|
||||
|
||||
class SetupFragmentMedia : Fragment() {
|
||||
var binding: FragmentSetupMediaBinding? = null
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_setup_media, container, false)
|
||||
): View {
|
||||
val localBinding = FragmentSetupMediaBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.fragment_setup_media, container, false)
|
||||
}
|
||||
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
context?.fixPaddingStatusbar(setup_root)
|
||||
normalSafeApiCall {
|
||||
fixPaddingStatusbar(binding?.setupRoot)
|
||||
|
||||
with(context) {
|
||||
if (this == null) return
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val ctx = context ?: return@normalSafeApiCall
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
val arrayAdapter =
|
||||
ArrayAdapter<String>(this, R.layout.sort_bottom_single_choice)
|
||||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
val names = enumValues<TvType>().sorted().map { it.name }
|
||||
val selected = mutableListOf<Int>()
|
||||
|
||||
arrayAdapter.addAll(names)
|
||||
listview1?.let {
|
||||
it.adapter = arrayAdapter
|
||||
it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||
binding?.apply {
|
||||
listview1.let {
|
||||
it.adapter = arrayAdapter
|
||||
it.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||
|
||||
it.setOnItemClickListener { _, _, _, _ ->
|
||||
it.checkedItemPositions?.forEach { key, value ->
|
||||
if (value) {
|
||||
selected.add(key)
|
||||
} else {
|
||||
selected.remove(key)
|
||||
it.setOnItemClickListener { _, _, _, _ ->
|
||||
it.checkedItemPositions?.forEach { key, value ->
|
||||
if (value) {
|
||||
selected.add(key)
|
||||
} else {
|
||||
selected.remove(key)
|
||||
}
|
||||
}
|
||||
val prefValues = selected.mapNotNull { pos ->
|
||||
val item =
|
||||
it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null
|
||||
val itemVal = TvType.valueOf(item)
|
||||
itemVal.ordinal.toString()
|
||||
}.toSet()
|
||||
settingsManager.edit()
|
||||
.putStringSet(getString(R.string.prefer_media_type_key), prefValues)
|
||||
.apply()
|
||||
|
||||
// Regenerate set homepage
|
||||
removeKey(USER_SELECTED_HOMEPAGE_API)
|
||||
}
|
||||
val prefValues = selected.mapNotNull { pos ->
|
||||
val item = it.getItemAtPosition(pos)?.toString() ?: return@mapNotNull null
|
||||
val itemVal = TvType.valueOf(item)
|
||||
itemVal.ordinal.toString()
|
||||
}.toSet()
|
||||
settingsManager.edit()
|
||||
.putStringSet(getString(R.string.prefer_media_type_key), prefValues)
|
||||
.apply()
|
||||
|
||||
// Regenerate set homepage
|
||||
removeKey(USER_SELECTED_HOMEPAGE_API)
|
||||
}
|
||||
}
|
||||
|
||||
next_btt?.setOnClickListener {
|
||||
findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout)
|
||||
}
|
||||
nextBtt.setOnClickListener {
|
||||
findNavController().navigate(R.id.navigation_setup_media_to_navigation_setup_layout)
|
||||
}
|
||||
|
||||
prev_btt?.setOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
prevBtt.setOnClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
|||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
|
@ -31,7 +32,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import kotlinx.android.synthetic.main.subtitle_settings.*
|
||||
|
||||
const val CHROME_SUBTITLE_KEY = "chome_subtitle_settings"
|
||||
|
||||
|
@ -137,12 +137,21 @@ class ChromecastSubtitlesFragment : Fragment() {
|
|||
//subtitle_text?.setStyle(fromSaveToStyle(state))
|
||||
}
|
||||
|
||||
var binding : ChromecastSubtitleSettingsBinding? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.chromecast_subtitle_settings, container, false)
|
||||
): View {
|
||||
val localBinding = ChromecastSubtitleSettingsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root//inflater.inflate(R.layout.chromecast_subtitle_settings, container, false)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private lateinit var state: SaveChromeCaptionStyle
|
||||
|
@ -159,7 +168,7 @@ class ChromecastSubtitlesFragment : Fragment() {
|
|||
onColorSelectedEvent += ::onColorSelected
|
||||
onDialogDismissedEvent += ::onDialogDismissed
|
||||
|
||||
context?.fixPaddingStatusbar(subs_root)
|
||||
fixPaddingStatusbar(binding?.subsRoot)
|
||||
|
||||
state = getCurrentSavedStyle()
|
||||
context?.updateState()
|
||||
|
@ -185,22 +194,25 @@ class ChromecastSubtitlesFragment : Fragment() {
|
|||
|
||||
this.setOnLongClickListener {
|
||||
it.context.setColor(id, null)
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
subs_text_color.setup(0)
|
||||
subs_outline_color.setup(1)
|
||||
subs_background_color.setup(2)
|
||||
binding?.apply {
|
||||
subsTextColor.setup(0)
|
||||
subsOutlineColor.setup(1)
|
||||
subsBackgroundColor.setup(2)
|
||||
}
|
||||
|
||||
|
||||
val dismissCallback = {
|
||||
if (hide)
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
|
||||
subs_edge_type.setFocusableInTv()
|
||||
subs_edge_type.setOnClickListener { textView ->
|
||||
binding?.subsEdgeType?.setFocusableInTv()
|
||||
binding?.subsEdgeType?.setOnClickListener { textView ->
|
||||
val edgeTypes = listOf(
|
||||
Pair(
|
||||
EDGE_TYPE_NONE,
|
||||
|
@ -237,15 +249,15 @@ class ChromecastSubtitlesFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
subs_edge_type.setOnLongClickListener {
|
||||
binding?.subsEdgeType?.setOnLongClickListener {
|
||||
state.edgeType = defaultState.edgeType
|
||||
it.context.updateState()
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subs_font_size.setFocusableInTv()
|
||||
subs_font_size.setOnClickListener { textView ->
|
||||
binding?.subsFontSize?.setFocusableInTv()
|
||||
binding?.subsFontSize?.setOnClickListener { textView ->
|
||||
val fontSizes = listOf(
|
||||
Pair(0.75f, "75%"),
|
||||
Pair(0.80f, "80%"),
|
||||
|
@ -278,24 +290,26 @@ class ChromecastSubtitlesFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
subs_font_size.setOnLongClickListener { _ ->
|
||||
binding?.subsFontSize?.setOnLongClickListener { _ ->
|
||||
state.fontScale = defaultState.fontScale
|
||||
//textView.context.updateState() // font size not changed
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subs_font.setFocusableInTv()
|
||||
subs_font.setOnClickListener { textView ->
|
||||
|
||||
|
||||
binding?.subsFont?.setFocusableInTv()
|
||||
binding?.subsFont?.setOnClickListener { textView ->
|
||||
val fontTypes = listOf(
|
||||
Pair(null, textView.context.getString(R.string.normal)),
|
||||
Pair("Droid Sans", "Droid Sans"),
|
||||
Pair("Droid Sans Mono", "Droid Sans Mono"),
|
||||
Pair("Droid Serif Regular", "Droid Serif Regular"),
|
||||
Pair("Cutive Mono", "Cutive Mono"),
|
||||
Pair("Short Stack", "Short Stack"),
|
||||
Pair("Quintessential", "Quintessential"),
|
||||
Pair("Alegreya Sans SC", "Alegreya Sans SC"),
|
||||
null to textView.context.getString(R.string.normal),
|
||||
"Droid Sans" to "Droid Sans",
|
||||
"Droid Sans Mono" to "Droid Sans Mono",
|
||||
"Droid Serif Regular" to "Droid Serif Regular",
|
||||
"Cutive Mono" to "Cutive Mono",
|
||||
"Short Stack" to "Short Stack",
|
||||
"Quintessential" to "Quintessential",
|
||||
"Alegreya Sans SC" to "Alegreya Sans SC",
|
||||
)
|
||||
|
||||
//showBottomDialog
|
||||
|
@ -310,35 +324,35 @@ class ChromecastSubtitlesFragment : Fragment() {
|
|||
textView.context.updateState()
|
||||
}
|
||||
}
|
||||
|
||||
subs_font.setOnLongClickListener { textView ->
|
||||
binding?.subsFont?.setOnLongClickListener { textView ->
|
||||
state.fontFamily = defaultState.fontFamily
|
||||
textView.context.updateState()
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
cancel_btt.setOnClickListener {
|
||||
binding?.cancelBtt?.setOnClickListener {
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
|
||||
apply_btt.setOnClickListener {
|
||||
binding?.applyBtt?.setOnClickListener {
|
||||
it.context.saveStyle(state)
|
||||
applyStyleEvent.invoke(state)
|
||||
//it.context.fromSaveToStyle(state)
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
|
||||
subtitle_text.setCues(
|
||||
listOf(
|
||||
Cue.Builder()
|
||||
.setTextSize(
|
||||
getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(),
|
||||
Cue.TEXT_SIZE_TYPE_ABSOLUTE
|
||||
)
|
||||
.setText(subtitle_text.context.getString(R.string.subtitles_example_text))
|
||||
.build()
|
||||
binding?.subtitleText?.apply {
|
||||
setCues(
|
||||
listOf(
|
||||
Cue.Builder()
|
||||
.setTextSize(
|
||||
getPixels(TypedValue.COMPLEX_UNIT_SP, 25.0f).toFloat(),
|
||||
Cue.TEXT_SIZE_TYPE_ABSOLUTE
|
||||
)
|
||||
.setText(context.getString(R.string.subtitles_example_text))
|
||||
.build()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent
|
|||
import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.Event
|
||||
|
@ -37,8 +38,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
|||
import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.navigate
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||
import kotlinx.android.synthetic.main.subtitle_settings.*
|
||||
import kotlinx.android.synthetic.main.toast.view.*
|
||||
import java.io.File
|
||||
|
||||
const val SUBTITLE_KEY = "subtitle_settings"
|
||||
|
@ -184,10 +183,10 @@ class SubtitlesFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun Context.updateState() {
|
||||
subtitle_text?.setStyle(fromSaveToStyle(state))
|
||||
val text = subtitle_text.context.getString(R.string.subtitles_example_text)
|
||||
binding?.subtitleText?.setStyle(fromSaveToStyle(state))
|
||||
val text = getString(R.string.subtitles_example_text)
|
||||
val fixedText = if (state.upperCase) text.uppercase() else text
|
||||
subtitle_text?.setCues(
|
||||
binding?.subtitleText?.setCues(
|
||||
listOf(
|
||||
Cue.Builder()
|
||||
.setTextSize(
|
||||
|
@ -213,12 +212,21 @@ class SubtitlesFragment : Fragment() {
|
|||
return if (color == Color.TRANSPARENT) Color.BLACK else color
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binding = null
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
var binding: SubtitleSettingsBinding? = null
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.subtitle_settings, container, false)
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val localBinding = SubtitleSettingsBinding.inflate(inflater, container, false)
|
||||
binding = localBinding
|
||||
return localBinding.root
|
||||
//return inflater.inflate(R.layout.subtitle_settings, container, false)
|
||||
}
|
||||
|
||||
private lateinit var state: SaveCaptionStyle
|
||||
|
@ -234,11 +242,11 @@ class SubtitlesFragment : Fragment() {
|
|||
hide = arguments?.getBoolean("hide") ?: true
|
||||
onColorSelectedEvent += ::onColorSelected
|
||||
onDialogDismissedEvent += ::onDialogDismissed
|
||||
subs_import_text?.text = getString(R.string.subs_import_text).format(
|
||||
binding?.subsImportText?.text = getString(R.string.subs_import_text).format(
|
||||
context?.getExternalFilesDir(null)?.absolutePath.toString() + "/Fonts"
|
||||
)
|
||||
|
||||
context?.fixPaddingStatusbar(subs_root)
|
||||
fixPaddingStatusbar(binding?.subsRoot)
|
||||
|
||||
state = getCurrentSavedStyle()
|
||||
context?.updateState()
|
||||
|
@ -264,317 +272,318 @@ class SubtitlesFragment : Fragment() {
|
|||
|
||||
this.setOnLongClickListener {
|
||||
it.context.setColor(id, null)
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
binding?.apply {
|
||||
subsTextColor.setup(0)
|
||||
subsOutlineColor.setup(1)
|
||||
subsBackgroundColor.setup(2)
|
||||
subsWindowColor.setup(3)
|
||||
|
||||
subs_text_color.setup(0)
|
||||
subs_outline_color.setup(1)
|
||||
subs_background_color.setup(2)
|
||||
subs_window_color.setup(3)
|
||||
|
||||
val dismissCallback = {
|
||||
if (hide)
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
|
||||
subs_subtitle_elevation.setFocusableInTv()
|
||||
subs_subtitle_elevation.setOnClickListener { textView ->
|
||||
val suffix = "dp"
|
||||
val elevationTypes = listOf(
|
||||
Pair(0, textView.context.getString(R.string.none)),
|
||||
Pair(10, "10$suffix"),
|
||||
Pair(20, "20$suffix"),
|
||||
Pair(30, "30$suffix"),
|
||||
Pair(40, "40$suffix"),
|
||||
Pair(50, "50$suffix"),
|
||||
Pair(60, "60$suffix"),
|
||||
Pair(70, "70$suffix"),
|
||||
Pair(80, "80$suffix"),
|
||||
Pair(90, "90$suffix"),
|
||||
Pair(100, "100$suffix"),
|
||||
)
|
||||
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
elevationTypes.map { it.second },
|
||||
elevationTypes.map { it.first }.indexOf(state.elevation),
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
state.elevation = elevationTypes.map { it.first }[index]
|
||||
textView.context.updateState()
|
||||
val dismissCallback = {
|
||||
if (hide)
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
}
|
||||
|
||||
subs_subtitle_elevation.setOnLongClickListener {
|
||||
state.elevation = DEF_SUBS_ELEVATION
|
||||
it.context.updateState()
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
subsSubtitleElevation.setFocusableInTv()
|
||||
subsSubtitleElevation.setOnClickListener { textView ->
|
||||
val suffix = "dp"
|
||||
val elevationTypes = listOf(
|
||||
Pair(0, textView.context.getString(R.string.none)),
|
||||
Pair(10, "10$suffix"),
|
||||
Pair(20, "20$suffix"),
|
||||
Pair(30, "30$suffix"),
|
||||
Pair(40, "40$suffix"),
|
||||
Pair(50, "50$suffix"),
|
||||
Pair(60, "60$suffix"),
|
||||
Pair(70, "70$suffix"),
|
||||
Pair(80, "80$suffix"),
|
||||
Pair(90, "90$suffix"),
|
||||
Pair(100, "100$suffix"),
|
||||
)
|
||||
|
||||
subs_edge_type.setFocusableInTv()
|
||||
subs_edge_type.setOnClickListener { textView ->
|
||||
val edgeTypes = listOf(
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_NONE,
|
||||
textView.context.getString(R.string.subtitles_none)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_OUTLINE,
|
||||
textView.context.getString(R.string.subtitles_outline)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_DEPRESSED,
|
||||
textView.context.getString(R.string.subtitles_depressed)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW,
|
||||
textView.context.getString(R.string.subtitles_shadow)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_RAISED,
|
||||
textView.context.getString(R.string.subtitles_raised)
|
||||
),
|
||||
)
|
||||
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
edgeTypes.map { it.second },
|
||||
edgeTypes.map { it.first }.indexOf(state.edgeType),
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
state.edgeType = edgeTypes.map { it.first }[index]
|
||||
textView.context.updateState()
|
||||
}
|
||||
}
|
||||
|
||||
subs_edge_type.setOnLongClickListener {
|
||||
state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE
|
||||
it.context.updateState()
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subs_font_size.setFocusableInTv()
|
||||
subs_font_size.setOnClickListener { textView ->
|
||||
val suffix = "sp"
|
||||
val fontSizes = listOf(
|
||||
Pair(null, textView.context.getString(R.string.normal)),
|
||||
Pair(6f, "6$suffix"),
|
||||
Pair(7f, "7$suffix"),
|
||||
Pair(8f, "8$suffix"),
|
||||
Pair(9f, "9$suffix"),
|
||||
Pair(10f, "10$suffix"),
|
||||
Pair(11f, "11$suffix"),
|
||||
Pair(12f, "12$suffix"),
|
||||
Pair(13f, "13$suffix"),
|
||||
Pair(14f, "14$suffix"),
|
||||
Pair(15f, "15$suffix"),
|
||||
Pair(16f, "16$suffix"),
|
||||
Pair(17f, "17$suffix"),
|
||||
Pair(18f, "18$suffix"),
|
||||
Pair(19f, "19$suffix"),
|
||||
Pair(20f, "20$suffix"),
|
||||
Pair(21f, "21$suffix"),
|
||||
Pair(22f, "22$suffix"),
|
||||
Pair(23f, "23$suffix"),
|
||||
Pair(24f, "24$suffix"),
|
||||
Pair(25f, "25$suffix"),
|
||||
Pair(26f, "26$suffix"),
|
||||
Pair(28f, "28$suffix"),
|
||||
Pair(30f, "30$suffix"),
|
||||
Pair(32f, "32$suffix"),
|
||||
Pair(34f, "34$suffix"),
|
||||
Pair(36f, "36$suffix"),
|
||||
Pair(38f, "38$suffix"),
|
||||
Pair(40f, "40$suffix"),
|
||||
Pair(42f, "42$suffix"),
|
||||
Pair(44f, "44$suffix"),
|
||||
Pair(48f, "48$suffix"),
|
||||
Pair(60f, "60$suffix"),
|
||||
)
|
||||
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
fontSizes.map { it.second },
|
||||
fontSizes.map { it.first }.indexOf(state.fixedTextSize),
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
state.fixedTextSize = fontSizes.map { it.first }[index]
|
||||
//textView.context.updateState() // font size not changed
|
||||
}
|
||||
}
|
||||
|
||||
subtitles_remove_bloat?.isChecked = state.removeBloat
|
||||
subtitles_remove_bloat?.setOnCheckedChangeListener { _, b ->
|
||||
state.removeBloat = b
|
||||
}
|
||||
subtitles_uppercase?.isChecked = state.upperCase
|
||||
subtitles_uppercase?.setOnCheckedChangeListener { _, b ->
|
||||
state.upperCase = b
|
||||
context?.updateState()
|
||||
}
|
||||
|
||||
subtitles_remove_captions?.isChecked = state.removeCaptions
|
||||
subtitles_remove_captions?.setOnCheckedChangeListener { _, b ->
|
||||
state.removeCaptions = b
|
||||
}
|
||||
|
||||
subs_font_size.setOnLongClickListener { _ ->
|
||||
state.fixedTextSize = null
|
||||
//textView.context.updateState() // font size not changed
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
//Fetch current value from preference
|
||||
context?.let { ctx ->
|
||||
subtitles_filter_sub_lang?.isChecked =
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.getBoolean(getString(R.string.filter_sub_lang_key), false)
|
||||
}
|
||||
|
||||
subtitles_filter_sub_lang?.setOnCheckedChangeListener { _, b ->
|
||||
context?.let { ctx ->
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.edit()
|
||||
.putBoolean(getString(R.string.filter_sub_lang_key), b)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
subs_font.setFocusableInTv()
|
||||
subs_font.setOnClickListener { textView ->
|
||||
val fontTypes = listOf(
|
||||
Pair(null, textView.context.getString(R.string.normal)),
|
||||
Pair(R.font.trebuchet_ms, "Trebuchet MS"),
|
||||
Pair(R.font.netflix_sans, "Netflix Sans"),
|
||||
Pair(R.font.google_sans, "Google Sans"),
|
||||
Pair(R.font.open_sans, "Open Sans"),
|
||||
Pair(R.font.futura, "Futura"),
|
||||
Pair(R.font.consola, "Consola"),
|
||||
Pair(R.font.gotham, "Gotham"),
|
||||
Pair(R.font.lucida_grande, "Lucida Grande"),
|
||||
Pair(R.font.stix_general, "STIX General"),
|
||||
Pair(R.font.times_new_roman, "Times New Roman"),
|
||||
Pair(R.font.verdana, "Verdana"),
|
||||
Pair(R.font.ubuntu_regular, "Ubuntu"),
|
||||
Pair(R.font.comic_sans, "Comic Sans"),
|
||||
Pair(R.font.poppins_regular, "Poppins"),
|
||||
)
|
||||
val savedFontTypes = textView.context.getSavedFonts()
|
||||
|
||||
val currentIndex =
|
||||
savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath }
|
||||
.let { index ->
|
||||
if (index == -1)
|
||||
fontTypes.indexOfFirst { it.first == state.typeface }
|
||||
else index + fontTypes.size
|
||||
}
|
||||
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
fontTypes.map { it.second } + savedFontTypes.map { it.name },
|
||||
currentIndex,
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
if (index < fontTypes.size) {
|
||||
state.typeface = fontTypes[index].first
|
||||
state.typefaceFilePath = null
|
||||
} else {
|
||||
state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath
|
||||
state.typeface = null
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
elevationTypes.map { it.second },
|
||||
elevationTypes.map { it.first }.indexOf(state.elevation),
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
state.elevation = elevationTypes.map { it.first }[index]
|
||||
textView.context.updateState()
|
||||
if (hide)
|
||||
activity?.hideSystemUI()
|
||||
}
|
||||
}
|
||||
|
||||
subsSubtitleElevation.setOnLongClickListener {
|
||||
state.elevation = DEF_SUBS_ELEVATION
|
||||
it.context.updateState()
|
||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subsEdgeType.setFocusableInTv()
|
||||
subsEdgeType.setOnClickListener { textView ->
|
||||
val edgeTypes = listOf(
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_NONE,
|
||||
textView.context.getString(R.string.subtitles_none)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_OUTLINE,
|
||||
textView.context.getString(R.string.subtitles_outline)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_DEPRESSED,
|
||||
textView.context.getString(R.string.subtitles_depressed)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW,
|
||||
textView.context.getString(R.string.subtitles_shadow)
|
||||
),
|
||||
Pair(
|
||||
CaptionStyleCompat.EDGE_TYPE_RAISED,
|
||||
textView.context.getString(R.string.subtitles_raised)
|
||||
),
|
||||
)
|
||||
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
edgeTypes.map { it.second },
|
||||
edgeTypes.map { it.first }.indexOf(state.edgeType),
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
state.edgeType = edgeTypes.map { it.first }[index]
|
||||
textView.context.updateState()
|
||||
}
|
||||
}
|
||||
|
||||
subsEdgeType.setOnLongClickListener {
|
||||
state.edgeType = CaptionStyleCompat.EDGE_TYPE_OUTLINE
|
||||
it.context.updateState()
|
||||
showToast(R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subsFontSize.setFocusableInTv()
|
||||
subsFontSize.setOnClickListener { textView ->
|
||||
val suffix = "sp"
|
||||
val fontSizes = listOf(
|
||||
Pair(null, textView.context.getString(R.string.normal)),
|
||||
Pair(6f, "6$suffix"),
|
||||
Pair(7f, "7$suffix"),
|
||||
Pair(8f, "8$suffix"),
|
||||
Pair(9f, "9$suffix"),
|
||||
Pair(10f, "10$suffix"),
|
||||
Pair(11f, "11$suffix"),
|
||||
Pair(12f, "12$suffix"),
|
||||
Pair(13f, "13$suffix"),
|
||||
Pair(14f, "14$suffix"),
|
||||
Pair(15f, "15$suffix"),
|
||||
Pair(16f, "16$suffix"),
|
||||
Pair(17f, "17$suffix"),
|
||||
Pair(18f, "18$suffix"),
|
||||
Pair(19f, "19$suffix"),
|
||||
Pair(20f, "20$suffix"),
|
||||
Pair(21f, "21$suffix"),
|
||||
Pair(22f, "22$suffix"),
|
||||
Pair(23f, "23$suffix"),
|
||||
Pair(24f, "24$suffix"),
|
||||
Pair(25f, "25$suffix"),
|
||||
Pair(26f, "26$suffix"),
|
||||
Pair(28f, "28$suffix"),
|
||||
Pair(30f, "30$suffix"),
|
||||
Pair(32f, "32$suffix"),
|
||||
Pair(34f, "34$suffix"),
|
||||
Pair(36f, "36$suffix"),
|
||||
Pair(38f, "38$suffix"),
|
||||
Pair(40f, "40$suffix"),
|
||||
Pair(42f, "42$suffix"),
|
||||
Pair(44f, "44$suffix"),
|
||||
Pair(48f, "48$suffix"),
|
||||
Pair(60f, "60$suffix"),
|
||||
)
|
||||
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
fontSizes.map { it.second },
|
||||
fontSizes.map { it.first }.indexOf(state.fixedTextSize),
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
state.fixedTextSize = fontSizes.map { it.first }[index]
|
||||
//textView.context.updateState() // font size not changed
|
||||
}
|
||||
}
|
||||
|
||||
subtitlesRemoveBloat.isChecked = state.removeBloat
|
||||
subtitlesRemoveBloat.setOnCheckedChangeListener { _, b ->
|
||||
state.removeBloat = b
|
||||
}
|
||||
subtitlesUppercase.isChecked = state.upperCase
|
||||
subtitlesUppercase.setOnCheckedChangeListener { _, b ->
|
||||
state.upperCase = b
|
||||
context?.updateState()
|
||||
}
|
||||
|
||||
subtitlesRemoveCaptions.isChecked = state.removeCaptions
|
||||
subtitlesRemoveCaptions.setOnCheckedChangeListener { _, b ->
|
||||
state.removeCaptions = b
|
||||
}
|
||||
|
||||
subsFontSize.setOnLongClickListener { _ ->
|
||||
state.fixedTextSize = null
|
||||
//textView.context.updateState() // font size not changed
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
//Fetch current value from preference
|
||||
context?.let { ctx ->
|
||||
subtitlesFilterSubLang.isChecked =
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.getBoolean(getString(R.string.filter_sub_lang_key), false)
|
||||
}
|
||||
|
||||
subtitlesFilterSubLang.setOnCheckedChangeListener { _, b ->
|
||||
context?.let { ctx ->
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.edit()
|
||||
.putBoolean(getString(R.string.filter_sub_lang_key), b)
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
subsFont.setFocusableInTv()
|
||||
subsFont.setOnClickListener { textView ->
|
||||
val fontTypes = listOf(
|
||||
Pair(null, textView.context.getString(R.string.normal)),
|
||||
Pair(R.font.trebuchet_ms, "Trebuchet MS"),
|
||||
Pair(R.font.netflix_sans, "Netflix Sans"),
|
||||
Pair(R.font.google_sans, "Google Sans"),
|
||||
Pair(R.font.open_sans, "Open Sans"),
|
||||
Pair(R.font.futura, "Futura"),
|
||||
Pair(R.font.consola, "Consola"),
|
||||
Pair(R.font.gotham, "Gotham"),
|
||||
Pair(R.font.lucida_grande, "Lucida Grande"),
|
||||
Pair(R.font.stix_general, "STIX General"),
|
||||
Pair(R.font.times_new_roman, "Times New Roman"),
|
||||
Pair(R.font.verdana, "Verdana"),
|
||||
Pair(R.font.ubuntu_regular, "Ubuntu"),
|
||||
Pair(R.font.comic_sans, "Comic Sans"),
|
||||
Pair(R.font.poppins_regular, "Poppins"),
|
||||
)
|
||||
val savedFontTypes = textView.context.getSavedFonts()
|
||||
|
||||
val currentIndex =
|
||||
savedFontTypes.indexOfFirst { it.absolutePath == state.typefaceFilePath }
|
||||
.let { index ->
|
||||
if (index == -1)
|
||||
fontTypes.indexOfFirst { it.first == state.typeface }
|
||||
else index + fontTypes.size
|
||||
}
|
||||
|
||||
//showBottomDialog
|
||||
activity?.showDialog(
|
||||
fontTypes.map { it.second } + savedFontTypes.map { it.name },
|
||||
currentIndex,
|
||||
(textView as TextView).text.toString(),
|
||||
false,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
if (index < fontTypes.size) {
|
||||
state.typeface = fontTypes[index].first
|
||||
state.typefaceFilePath = null
|
||||
} else {
|
||||
state.typefaceFilePath = savedFontTypes[index - fontTypes.size].absolutePath
|
||||
state.typeface = null
|
||||
}
|
||||
textView.context.updateState()
|
||||
}
|
||||
}
|
||||
|
||||
subsFont.setOnLongClickListener { textView ->
|
||||
state.typeface = null
|
||||
state.typefaceFilePath = null
|
||||
textView.context.updateState()
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
subs_font.setOnLongClickListener { textView ->
|
||||
state.typeface = null
|
||||
state.typefaceFilePath = null
|
||||
textView.context.updateState()
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
subsAutoSelectLanguage.setFocusableInTv()
|
||||
subsAutoSelectLanguage.setOnClickListener { textView ->
|
||||
val langMap = arrayListOf(
|
||||
SubtitleHelper.Language639(
|
||||
textView.context.getString(R.string.none),
|
||||
textView.context.getString(R.string.none),
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
),
|
||||
)
|
||||
langMap.addAll(SubtitleHelper.languages)
|
||||
|
||||
subs_auto_select_language.setFocusableInTv()
|
||||
subs_auto_select_language.setOnClickListener { textView ->
|
||||
val langMap = arrayListOf(
|
||||
SubtitleHelper.Language639(
|
||||
textView.context.getString(R.string.none),
|
||||
textView.context.getString(R.string.none),
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
""
|
||||
),
|
||||
)
|
||||
langMap.addAll(SubtitleHelper.languages)
|
||||
|
||||
val lang639_1 = langMap.map { it.ISO_639_1 }
|
||||
activity?.showDialog(
|
||||
langMap.map { it.languageName },
|
||||
lang639_1.indexOf(getAutoSelectLanguageISO639_1()),
|
||||
(textView as TextView).text.toString(),
|
||||
true,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
|
||||
val lang639_1 = langMap.map { it.ISO_639_1 }
|
||||
activity?.showDialog(
|
||||
langMap.map { it.languageName },
|
||||
lang639_1.indexOf(getAutoSelectLanguageISO639_1()),
|
||||
(textView as TextView).text.toString(),
|
||||
true,
|
||||
dismissCallback
|
||||
) { index ->
|
||||
setKey(SUBTITLE_AUTO_SELECT_KEY, lang639_1[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subs_auto_select_language.setOnLongClickListener {
|
||||
setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
subs_download_languages.setFocusableInTv()
|
||||
subs_download_languages.setOnClickListener { textView ->
|
||||
val langMap = SubtitleHelper.languages
|
||||
val lang639_1 = langMap.map { it.ISO_639_1 }
|
||||
val keys = getDownloadSubsLanguageISO639_1()
|
||||
val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 }
|
||||
|
||||
activity?.showMultiDialog(
|
||||
langMap.map { it.languageName },
|
||||
keyMap,
|
||||
(textView as TextView).text.toString(),
|
||||
dismissCallback
|
||||
) { indexList ->
|
||||
setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
|
||||
subsAutoSelectLanguage.setOnLongClickListener {
|
||||
setKey(SUBTITLE_AUTO_SELECT_KEY, "en")
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
}
|
||||
|
||||
subs_download_languages.setOnLongClickListener {
|
||||
setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
|
||||
subsDownloadLanguages.setFocusableInTv()
|
||||
subsDownloadLanguages.setOnClickListener { textView ->
|
||||
val langMap = SubtitleHelper.languages
|
||||
val lang639_1 = langMap.map { it.ISO_639_1 }
|
||||
val keys = getDownloadSubsLanguageISO639_1()
|
||||
val keyMap = keys.map { lang639_1.indexOf(it) }.filter { it >= 0 }
|
||||
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
activity?.showMultiDialog(
|
||||
langMap.map { it.languageName },
|
||||
keyMap,
|
||||
(textView as TextView).text.toString(),
|
||||
dismissCallback
|
||||
) { indexList ->
|
||||
setKey(SUBTITLE_DOWNLOAD_KEY, indexList.map { lang639_1[it] }.toList())
|
||||
}
|
||||
}
|
||||
|
||||
cancel_btt.setOnClickListener {
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
subsDownloadLanguages.setOnLongClickListener {
|
||||
setKey(SUBTITLE_DOWNLOAD_KEY, listOf("en"))
|
||||
|
||||
apply_btt.setOnClickListener {
|
||||
it.context.saveStyle(state)
|
||||
applyStyleEvent.invoke(state)
|
||||
it.context.fromSaveToStyle(state)
|
||||
activity?.popCurrentPage()
|
||||
showToast(activity, R.string.subs_default_reset_toast, Toast.LENGTH_SHORT)
|
||||
return@setOnLongClickListener true
|
||||
}
|
||||
|
||||
cancelBtt.setOnClickListener {
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
|
||||
applyBtt.setOnClickListener {
|
||||
it.context.saveStyle(state)
|
||||
applyStyleEvent.invoke(state)
|
||||
it.context.fromSaveToStyle(state)
|
||||
activity?.popCurrentPage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,9 @@ import android.os.*
|
|||
import android.provider.MediaStore
|
||||
import android.text.Spanned
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.View.LAYOUT_DIRECTION_LTR
|
||||
import android.view.View.LAYOUT_DIRECTION_RTL
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -33,6 +36,7 @@ import androidx.core.widget.ContentLoadingProgressBar
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -47,12 +51,14 @@ import com.google.android.gms.common.GoogleApiAvailability
|
|||
import com.google.android.gms.common.wrappers.Wrappers
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStringResumeWatching
|
||||
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
|
||||
import com.lagradost.cloudstream3.ui.WebviewFragment
|
||||
import com.lagradost.cloudstream3.ui.result.ResultFragment
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
|
||||
|
@ -87,6 +93,9 @@ object AppUtils {
|
|||
return if (layoutManager == null || adapter == null) false else layoutManager.findLastCompletelyVisibleItemPosition() < adapter.itemCount - 7 // bit more than 1 to make it more seamless
|
||||
}
|
||||
|
||||
fun View.isLtr() = this.layoutDirection == LAYOUT_DIRECTION_LTR
|
||||
fun View.isRtl() = this.layoutDirection == LAYOUT_DIRECTION_RTL
|
||||
|
||||
fun BottomSheetDialog?.ownHide() {
|
||||
this?.hide()
|
||||
}
|
||||
|
@ -198,7 +207,11 @@ object AppUtils {
|
|||
animation.start()
|
||||
}
|
||||
|
||||
fun Context.createNotificationChannel(channelId: String, channelName: String, description: String) {
|
||||
fun Context.createNotificationChannel(
|
||||
channelId: String,
|
||||
channelName: String,
|
||||
description: String
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
||||
val channel =
|
||||
|
@ -288,6 +301,7 @@ object AppUtils {
|
|||
|
||||
// https://github.com/googlearchive/leanback-homescreen-channels/blob/master/app/src/main/java/com/google/android/tvhomescreenchannels/SampleTvProvider.java
|
||||
@SuppressLint("RestrictedApi")
|
||||
@Throws
|
||||
@WorkerThread
|
||||
suspend fun Context.addProgramsToContinueWatching(data: List<DataStoreHelper.ResumeWatchingResult>) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
|
@ -369,7 +383,6 @@ object AppUtils {
|
|||
)
|
||||
main {
|
||||
showToast(
|
||||
this@loadRepository,
|
||||
getString(R.string.player_loaded_subtitles, repo.name),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
|
@ -577,12 +590,29 @@ object AppUtils {
|
|||
}
|
||||
}
|
||||
|
||||
fun loadResult(
|
||||
url: String,
|
||||
apiName: String,
|
||||
startAction: Int = 0,
|
||||
startValue: Int = 0
|
||||
) {
|
||||
(activity as FragmentActivity?)?.loadResult(url, apiName, startAction, startValue)
|
||||
}
|
||||
|
||||
fun FragmentActivity.loadResult(
|
||||
url: String,
|
||||
apiName: String,
|
||||
startAction: Int = 0,
|
||||
startValue: Int = 0
|
||||
) {
|
||||
try {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
Kitsu.isEnabled =
|
||||
settingsManager.getBoolean(this.getString(R.string.show_kitsu_posters_key), true)
|
||||
}catch (t : Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
|
||||
this.runOnUiThread {
|
||||
// viewModelStore.clear()
|
||||
this.navigate(
|
||||
|
@ -592,6 +622,14 @@ object AppUtils {
|
|||
}
|
||||
}
|
||||
|
||||
fun loadSearchResult(
|
||||
card: SearchResponse,
|
||||
startAction: Int = 0,
|
||||
startValue: Int? = null,
|
||||
) {
|
||||
activity?.loadSearchResult(card, startAction, startValue)
|
||||
}
|
||||
|
||||
fun Activity?.loadSearchResult(
|
||||
card: SearchResponse,
|
||||
startAction: Int = 0,
|
||||
|
@ -776,12 +814,12 @@ object AppUtils {
|
|||
return networkInfo.any {
|
||||
conManager.getNetworkCapabilities(it)
|
||||
?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
|
||||
} &&
|
||||
} &&
|
||||
!networkInfo.any {
|
||||
conManager.getNetworkCapabilities(it)
|
||||
?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) == true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun Activity?.cacheClass(clazz: String?) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -399,6 +399,7 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
|||
Moviehab(),
|
||||
MoviehabNet(),
|
||||
Jeniusplay(),
|
||||
StreamoUpload(),
|
||||
|
||||
Gdriveplayerapi(),
|
||||
Gdriveplayerapp(),
|
||||
|
@ -548,4 +549,4 @@ abstract class ExtractorApi {
|
|||
open fun getExtractorUrl(id: String): String {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -2,19 +2,28 @@ package com.lagradost.cloudstream3.utils
|
|||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import android.widget.AbsListView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.*
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginLeft
|
||||
import androidx.core.view.marginRight
|
||||
import androidx.core.view.marginTop
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||
import kotlinx.android.synthetic.main.add_account_input.*
|
||||
import kotlinx.android.synthetic.main.add_account_input.text1
|
||||
import kotlinx.android.synthetic.main.bottom_selection_dialog_direct.*
|
||||
|
||||
object SingleSelectionHelper {
|
||||
fun Activity?.showOptionSelectStringRes(
|
||||
|
@ -82,6 +91,7 @@ object SingleSelectionHelper {
|
|||
}
|
||||
|
||||
fun Activity?.showDialog(
|
||||
binding: BottomSelectionDialogBinding,
|
||||
dialog: Dialog,
|
||||
items: List<String>,
|
||||
selectedIndex: List<Int>,
|
||||
|
@ -95,39 +105,39 @@ object SingleSelectionHelper {
|
|||
if (this == null) return
|
||||
|
||||
val realShowApply = showApply || isMultiSelect
|
||||
val listView = dialog.listview1//.findViewById<ListView>(R.id.listview1)!!
|
||||
val textView = dialog.text1//.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = dialog.apply_btt//.findViewById<TextView>(R.id.apply_btt)
|
||||
val cancelButton = dialog.cancel_btt//findViewById<TextView>(R.id.cancel_btt)
|
||||
val listView = binding.listview1//.findViewById<ListView>(R.id.listview1)!!
|
||||
val textView = binding.text1//.findViewById<TextView>(R.id.text1)!!
|
||||
val applyButton = binding.applyBtt//.findViewById<TextView>(R.id.apply_btt)
|
||||
val cancelButton = binding.cancelBtt//findViewById<TextView>(R.id.cancel_btt)
|
||||
val applyHolder =
|
||||
dialog.apply_btt_holder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
|
||||
binding.applyBttHolder//.findViewById<LinearLayout>(R.id.apply_btt_holder)
|
||||
|
||||
applyHolder?.isVisible = realShowApply
|
||||
applyHolder.isVisible = realShowApply
|
||||
if (!realShowApply) {
|
||||
val params = listView.layoutParams as LinearLayout.LayoutParams
|
||||
params.setMargins(listView.marginLeft, listView.marginTop, listView.marginRight, 0)
|
||||
listView.layoutParams = params
|
||||
}
|
||||
|
||||
textView?.text = name
|
||||
textView?.isGone = name.isBlank()
|
||||
textView.text = name
|
||||
textView.isGone = name.isBlank()
|
||||
|
||||
val arrayAdapter = ArrayAdapter<String>(this, itemLayout)
|
||||
arrayAdapter.addAll(items)
|
||||
|
||||
listView?.adapter = arrayAdapter
|
||||
listView.adapter = arrayAdapter
|
||||
if (isMultiSelect) {
|
||||
listView?.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_MULTIPLE
|
||||
} else {
|
||||
listView?.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
listView.choiceMode = AbsListView.CHOICE_MODE_SINGLE
|
||||
}
|
||||
|
||||
for (select in selectedIndex) {
|
||||
listView?.setItemChecked(select, true)
|
||||
listView.setItemChecked(select, true)
|
||||
}
|
||||
|
||||
selectedIndex.minOrNull()?.let {
|
||||
listView?.setSelection(it)
|
||||
listView.setSelection(it)
|
||||
}
|
||||
|
||||
// var lastSelectedIndex = if(selectedIndex.isNotEmpty()) selectedIndex.first() else -1
|
||||
|
@ -136,7 +146,7 @@ object SingleSelectionHelper {
|
|||
dismissCallback.invoke()
|
||||
}
|
||||
|
||||
listView?.setOnItemClickListener { _, _, which, _ ->
|
||||
listView.setOnItemClickListener { _, _, which, _ ->
|
||||
// lastSelectedIndex = which
|
||||
if (realShowApply) {
|
||||
if (!isMultiSelect) {
|
||||
|
@ -148,7 +158,7 @@ object SingleSelectionHelper {
|
|||
}
|
||||
}
|
||||
if (realShowApply) {
|
||||
applyButton?.setOnClickListener {
|
||||
applyButton.setOnClickListener {
|
||||
val list = ArrayList<Int>()
|
||||
for (index in 0 until listView.count) {
|
||||
if (listView.checkedItemPositions[index])
|
||||
|
@ -157,7 +167,7 @@ object SingleSelectionHelper {
|
|||
callback.invoke(list)
|
||||
dialog.dismissSafe(this)
|
||||
}
|
||||
cancelButton?.setOnClickListener {
|
||||
cancelButton.setOnClickListener {
|
||||
dialog.dismissSafe(this)
|
||||
}
|
||||
}
|
||||
|
@ -213,13 +223,26 @@ object SingleSelectionHelper {
|
|||
) {
|
||||
if (this == null) return
|
||||
|
||||
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
|
||||
LayoutInflater.from(this)
|
||||
)
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.bottom_selection_dialog)
|
||||
.setView(binding.root)
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
showDialog(dialog, items, selectedIndex, name, true, true, callback, dismissCallback)
|
||||
showDialog(
|
||||
binding,
|
||||
dialog,
|
||||
items,
|
||||
selectedIndex,
|
||||
name,
|
||||
showApply = true,
|
||||
isMultiSelect = true,
|
||||
callback,
|
||||
dismissCallback
|
||||
)
|
||||
}
|
||||
|
||||
fun Activity?.showDialog(
|
||||
|
@ -232,13 +255,19 @@ object SingleSelectionHelper {
|
|||
) {
|
||||
if (this == null) return
|
||||
|
||||
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
|
||||
LayoutInflater.from(this)
|
||||
)
|
||||
val builder =
|
||||
AlertDialog.Builder(this, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.bottom_selection_dialog)
|
||||
.setView(binding.root)
|
||||
|
||||
val dialog = builder.create()
|
||||
dialog.show()
|
||||
|
||||
|
||||
showDialog(
|
||||
binding,
|
||||
dialog,
|
||||
items,
|
||||
listOf(selectedIndex),
|
||||
|
@ -260,12 +289,18 @@ object SingleSelectionHelper {
|
|||
callback: (Int) -> Unit,
|
||||
) {
|
||||
if (this == null) return
|
||||
|
||||
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
|
||||
LayoutInflater.from(this)
|
||||
)
|
||||
|
||||
val builder =
|
||||
BottomSheetDialog(this)
|
||||
builder.setContentView(R.layout.bottom_selection_dialog)
|
||||
builder.setContentView(binding.root)
|
||||
|
||||
builder.show()
|
||||
showDialog(
|
||||
binding,
|
||||
builder,
|
||||
items,
|
||||
listOf(selectedIndex),
|
||||
|
@ -285,13 +320,19 @@ object SingleSelectionHelper {
|
|||
): BottomSheetDialog {
|
||||
val builder =
|
||||
BottomSheetDialog(this)
|
||||
builder.setContentView(R.layout.bottom_selection_dialog_direct)
|
||||
|
||||
val binding: BottomSelectionDialogBinding = BottomSelectionDialogBinding.inflate(
|
||||
LayoutInflater.from(this)
|
||||
)
|
||||
|
||||
//builder.setContentView(R.layout.bottom_selection_dialog_direct)
|
||||
builder.setContentView(binding.root)
|
||||
builder.show()
|
||||
showDialog(
|
||||
binding,
|
||||
builder,
|
||||
items,
|
||||
listOf(),
|
||||
emptyList(),
|
||||
name,
|
||||
showApply = false,
|
||||
isMultiSelect = false,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue